前後端分離整合SpringSecurity

qq_41498751發表於2020-12-11

簡單的前後端分離整合SpringSecurity

推薦學習地址:https://blog.csdn.net/u012702547/article/details/79019510

在普通的專案中使用SpringSecurity比較簡單,但是在前後端分離的專案中使用SpringSecurity就比較複雜了

總體步驟:

  • 建立簡易的資料庫(使用者表、許可權表等)

  • 建立一個簡單的Vue登入頁面

  • 編寫實體類

  • 編寫登入的dao、service等

  • 在Controller中編寫一個@GetMapping("/login")的控制器

  • 重點來了
    + 在實體類中實現UserDetails
    + 編寫UserDetailsService實現loadUserByUsername對輸入的使用者名稱、密碼進行驗證
    + 編寫主配置類,配置密碼加密、登入、攔截、登出等功能
    + 編寫輔類,主要用於顯示許可權不足、是否登入、許可權等等一些列的json資料

程式碼

  1. 資料庫(省略)
  2. 建立一個簡單的Vue登入頁面
<template>
  <div class="login-div">
    <div class="img"></div>
    <el-form ref="loginForm" :model="form" :rules="rules" label-width="70px" class="login-box" >
      <div class="form">
        <h3 class="login-title">歡迎登入</h3>
        <el-form-item label="賬號" prop="username">
          <el-input
            type="text"
            placeholder="請輸入賬號"
            v-model="form.username"
          />
        </el-form-item>
        <el-form-item label="密碼" prop="password">
          <el-input
            type="password"
            placeholder="請輸入密碼"
            v-model="form.password"
          />
        </el-form-item>
        <el-form-item class="yzm" label="驗證碼" prop="yzm">
          <el-input type="text" placeholder="請輸入驗證碼" v-model="form.yzm" />
          <div class="vc">
            <img
              :src="'http://127.0.0.1:8080/api/users/umsUser/captcha.jpg'"
              alt=""
            />
            <span>&nbsp;<a href="">看不清?</a></span>
          </div>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" v-on:click="onSubmit('loginForm')"
            >登入</el-button
          >
        </el-form-item>
      </div>
    </el-form>

    <el-dialog title="溫馨提示" :visible.sync="dialogVisible" width="30%">
      <span>請輸入賬號和密碼</span>
      <span slot="footer" class="dialog-footer">
        <el-button type="primary" @click="dialogVisible = false"
          >確 定</el-button
        >
      </span>
    </el-dialog>
  </div>
</template>

<script>
import qs from "qs";
export default {
  name: "Login",
  data() {
    return {
      form: {
        username: "",
        password: "",
        yzm: "",
      },
      imgsrc: "",
      // 表單驗證,需要在 el-form-item 元素中增加 prop 屬性
      rules: {
        username: [
          { required: true, message: "賬號不可為空", trigger: "blur" },
        ],
        password: [
          { required: true, message: "密碼不可為空", trigger: "blur" },
        ],
        yzm: [{ required: true, message: "驗證碼不能為空", trigger: "blur" }],
      },
      // 對話方塊顯示和隱藏
      dialogVisible: false,
    };
  },
  methods: {
    onSubmit(formName) {
      // 為表單繫結驗證功能
      this.$refs[formName].validate((valid) => {
        if (valid) {
          //傳送登入請求
          var form = new window.FormData();
          form.append("username", "laotie");
          let url =`/api/users/login?username=` +this.form.username +"&password=" +this.form.password;
           var _this=this;
          this.$http.post(url,qs.stringify(form),
              {
                //設定請求頭,否則後臺獲取不到資料
                headers: {
                  "Content-Type": "application/x-www-form-urlencoded",
                },
              },
              {}
            )
            .then(function (data) {
              if (data.data.status == "success") {
                _this.$message({
                  message: '登入成功,歡迎回來:'+_this.form.username,
                  type: 'success'
                });
                _this.$router.push("/about");
              }else{
                console.log('登入失敗');
                  this.$message.error('登入失敗');
              }
            });
        }
      });
    },
  },
};
</script>

<style lang="scss" scoped>
.login-box {
  width: 400px;
  padding: 35px 35px 15px 35px;
  border-radius: 5px;
  margin-top: 25px;
  margin: 0 auto;
}

.login-title {
  text-align: center;
  margin: 8px auto;
  color: #303133;
}
.login-div {
  display: flex;
  align-items: center;
  position: relative;
  top: 60px;
}
.img {
  width: 50%;
  height: 500px;
  background-image: url("../assets/background.jpg");
  background-size: 100% 100%;
}
.form {
  position: relative;
  right: 160px;
}
.vc {
  margin-top: 5px;
}
</style>
  1. 編寫實體類(省略)
  2. 編寫登入的dao、service等就是簡單的根據使用者名稱查詢使用者資訊(省略)
  3. 在Controller中編寫一個@GetMapping("/login")的控制器
    @GetMapping("/login")
    public R login() {
        return R.failed("尚未登入,請登入!");
    }
  1. 開始配置SpringSecurity

    1. 在實體類中實現UserDetails
    public class UmsUser extends Model<UmsUser> implements UserDetails {
    	......
            
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            List<GrantedAuthority> authorities = new ArrayList<>();
    //        for (Role role : roles) {
    //            authorities.add(new SimpleGrantedAuthority(role.getName()));
    //        }
            return authorities;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return false;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return false;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return false;
        }
    
        @Override
        public boolean isEnabled() {
            return false;
        }
    }
    
    1. 編寫UserDetailsService實現loadUserByUsername對輸入的使用者名稱、密碼進行驗證
    @Slf4j
    @Service
    public class SecurityUserDatilService implements UserDetailsService {
    
        @Resource
        private UmsUserService umsUserService;
    
        /**
         * 登入
         * @param username
         * @return
         */
        @Override
        public UserDetails loadUserByUsername(String username) {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            //獲取驗證碼此處可以省略
            Object captcha = request.getSession().getAttribute("captcha");
            System.out.println("captcha = " + captcha);
            //輸出使用者名稱
            System.out.println("username = " +username);
            //設定許可權
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            //根據使用者名稱查詢使用者資訊
            QueryWrapper<UmsUser> umsUserQueryWrapper = new QueryWrapper<>();
            umsUserQueryWrapper.eq("username", username);
            UmsUser umsUser = this.umsUserService.getOne(umsUserQueryWrapper);
            //判斷使用者是否存在
            if (umsUser == null) {
                System.out.println("使用者名稱不對" );
                throw new UsernameNotFoundException("使用者名稱不對");
            }
            //賦予登入許可權
            authorities.add(new SimpleGrantedAuthority("ROLE_LOGIN"));
            return new User(username,umsUser.getPassword(),authorities);
        }
    }
    
    
    1. 重要:編寫主配置類,配置密碼加密、登入、攔截、登出等功能
    package com.swj.securiy;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationProvider;
    import org.springframework.security.authentication.BadCredentialsException;
    import org.springframework.security.authentication.DisabledException;
    import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
    import org.springframework.security.config.annotation.ObjectPostProcessor;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    
    import javax.annotation.Resource;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Resource
        private  SecurityUserDatilService securityUserDatilService;
        @Resource
        UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource;
        @Resource
        UrlAccessDecisionManager urlAccessDecisionManager;
        @Resource
        AuthenticationAccessDeniedHandler authenticationAccessDeniedHandler;
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(securityUserDatilService).passwordEncoder(passwordEncoder());
        }
    
        /**
         * 攔截器
         * @param web
         * @throws Exception
         */
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers( "/static/**","/umsUser/login","/umsUser/captcha.jpg");
        }
    
        /**
         * 對登入失敗返回指定的json格式的資料
         * @param http
         * @throws Exception
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            http.authorizeRequests()
                    .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                        @Override
                        public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                            o.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource);
                            o.setAccessDecisionManager(urlAccessDecisionManager);
                            return o;
                        }
                    }).and().formLogin().usernameParameter("username").passwordParameter("password").permitAll().failureHandler(new AuthenticationFailureHandler() {
                @Override
                public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                    //登入失敗
                    httpServletResponse.setContentType("application/json;charset=utf-8");
                    PrintWriter out = httpServletResponse.getWriter();
                    StringBuffer sb = new StringBuffer();
                    sb.append("{\"status\":\"error\",\"msg\":\"");
                    if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
                        sb.append("使用者名稱或密碼輸入錯誤,登入失敗!");
                    } else if (e instanceof DisabledException) {
                        sb.append("賬戶被禁用,登入失敗,請聯絡管理員!");
                    } else {
                        sb.append("登入失敗!");
                    }
                    sb.append("\"}");
                    out.write(sb.toString());
                    out.flush();
                    out.close();
                }
            }).successHandler(new AuthenticationSuccessHandler() {
                //登入成功
                @Override
                public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                    httpServletResponse.setContentType("application/json;charset=utf-8");
                    PrintWriter out = httpServletResponse.getWriter();
                    ObjectMapper objectMapper = new ObjectMapper();
                    String s = "{\"status\":\"success\",\"msg\":\"恭喜你登入成功了\"}";
                    out.write(s);
                    out.flush();
                    out.close();
                }
                //設定登出、跨域等資訊
            }).and().logout().permitAll().and().csrf().disable().exceptionHandling().accessDeniedHandler(authenticationAccessDeniedHandler);
        }
    
        /**
         * 密碼加密
         * @return
         */
        @Bean
        BCryptPasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
    }
    
    
    1. 編寫輔類,主要用於顯示許可權不足、是否登入、許可權等等一些列的json資料

      • AuthenticationAccessDeniedHandler
      package com.swj.securiy;
      
      import org.springframework.security.access.AccessDeniedException;
      import org.springframework.security.web.access.AccessDeniedHandler;
      import org.springframework.stereotype.Component;
      
      import javax.servlet.ServletException;
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import java.io.IOException;
      import java.io.PrintWriter;
      
      @Component
      public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {
          @Override
          public void handle(HttpServletRequest httpServletRequest, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException {
              resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
              resp.setCharacterEncoding("UTF-8");
              PrintWriter out = resp.getWriter();
              out.write("{\"status\":\"error\",\"msg\":\"許可權不足,請聯絡管理員!\"}");
              out.flush();
              out.close();
          }
      }
      
      
      • UrlFilterInvocationSecurityMetadataSource
      package com.swj.securiy;
      
      import org.springframework.security.access.ConfigAttribute;
      import org.springframework.security.web.FilterInvocation;
      import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
      import org.springframework.stereotype.Component;
      import org.springframework.security.access.SecurityConfig;
      
      import javax.annotation.Resource;
      import java.util.Collection;
      
      @Component
      public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
      
          @Override
          public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
              String requestUrl = ((FilterInvocation) o).getRequestUrl();
              return SecurityConfig.createList("ROLE_LOGIN");
          }
      
          @Override
          public Collection<ConfigAttribute> getAllConfigAttributes() {
              return null;
          }
      
          @Override
          public boolean supports(Class<?> aClass) {
              return true;
          }
      }
      
      
      • UrlAccessDecisionManager
      package com.swj.securiy;
      
      import org.springframework.security.access.AccessDecisionManager;
      import org.springframework.security.access.AccessDeniedException;
      import org.springframework.security.access.ConfigAttribute;
      import org.springframework.security.authentication.AnonymousAuthenticationToken;
      import org.springframework.security.authentication.BadCredentialsException;
      import org.springframework.security.authentication.InsufficientAuthenticationException;
      import org.springframework.security.core.Authentication;
      import org.springframework.security.core.AuthenticationException;
      import org.springframework.security.core.GrantedAuthority;
      import org.springframework.stereotype.Component;
      
      import java.util.Collection;
      import java.util.Iterator;
      
      @Component
      public class UrlAccessDecisionManager implements AccessDecisionManager {
      
          @Override
          public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection)
                  throws AccessDeniedException, AuthenticationException {
              Iterator<ConfigAttribute> iterator = collection.iterator();
              while (iterator.hasNext()) {
                  ConfigAttribute ca = iterator.next();
                  //當前請求需要的許可權
                  String needRole = ca.getAttribute();
                  if ("ROLE_LOGIN".equals(needRole)) {
                      if (authentication instanceof AnonymousAuthenticationToken) {
                          throw new BadCredentialsException("未登入");
                      } else
                          return;
                  }
                  //當前使用者所具有的許可權
                  Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
                  for (GrantedAuthority authority : authorities) {
                      if (authority.getAuthority().equals(needRole)) {
                          return;
                      }
                  }
              }
              throw new AccessDeniedException("許可權不足!");
          }
      
          @Override
          public boolean supports(ConfigAttribute configAttribute) {
              return true;
          }
      
          @Override
          public boolean supports(Class<?> aClass) {
              return true;
          }
      }
      
      

到此結束

總結:其實程式碼沒有那麼複雜,可以對部門程式碼進行省略,比如輔助類個別的就可以對其省略,完全沒有必要寫,因為像UrlFilterInvocationSecurityMetadataSource,可以對程式碼進行簡化,在主配置中的登入成功、登入失敗返回的資料完全可以單獨提成兩個方法。SpringSecurity前後端分離還是很有必要學習一下的。

相關文章