Spring Security 自定義登錄認證(二)

  • 2019 年 10 月 14 日
  • 筆記

一、前言

本篇文章將講述Spring Security自定義登錄認證校驗用戶名、密碼,自定義密碼加密方式,以及在前後端分離的情況下認證失敗或成功處理返回json格式數據

溫馨小提示:Spring Security中有默認的密碼加密方式以及登錄用戶認證校驗,但小編這裡選擇自定義是為了方便以後業務擴展,比如系統默認帶一個超級管理員,當認證時識別到是超級管理員帳號登錄訪問時給它賦予最高許可權,可以訪問系統所有api介面,或在登錄認證成功後存入token以便用戶訪問系統其它介面時通過token認證用戶許可權等

Spring Security入門學習可參考之前文章:

SpringBoot集成Spring Security入門體驗(一)

https://blog.csdn.net/qq_38225558/article/details/101754743

二、Spring Security 自定義登錄認證處理

基本環境
  1. spring-boot 2.1.8
  2. mybatis-plus 2.2.0
  3. mysql
  4. maven項目

資料庫用戶資訊表t_sys_user
在這裡插入圖片描述

案例中關於對該t_sys_user用戶表相關的增刪改查程式碼就不貼出來了,如有需要可參考文末提供的案例demo源碼

1、Security 核心配置類

配置用戶密碼校驗過濾器

@Configuration  @EnableWebSecurity  @EnableGlobalMethodSecurity(prePostEnabled = true)  public class SecurityConfig extends WebSecurityConfigurerAdapter {        /**       * 用戶密碼校驗過濾器       */      private final AdminAuthenticationProcessingFilter adminAuthenticationProcessingFilter;        public SecurityConfig(AdminAuthenticationProcessingFilter adminAuthenticationProcessingFilter) {          this.adminAuthenticationProcessingFilter = adminAuthenticationProcessingFilter;      }        /**       * 許可權配置       * @param http       * @throws Exception       */      @Override      protected void configure(HttpSecurity http) throws Exception {          ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.antMatcher("/**").authorizeRequests();            // 禁用CSRF 開啟跨域          http.csrf().disable().cors();            // 登錄處理 - 前後端一體的情況下  //        registry.and().formLogin().loginPage("/login").defaultSuccessUrl("/").permitAll()  //                // 自定義登陸用戶名和密碼屬性名,默認為 username和password  //                .usernameParameter("username").passwordParameter("password")  //                // 異常處理  //                .failureUrl("/login/error").permitAll()  //                // 退出登錄  //                .and().logout().permitAll();            // 標識只能在 伺服器本地ip[127.0.0.1或localhost] 訪問`/home`介面,其他ip地址無法訪問          registry.antMatchers("/home").hasIpAddress("127.0.0.1");          // 允許匿名的url - 可理解為放行介面 - 多個介面使用,分割          registry.antMatchers("/login", "/index").permitAll();          // OPTIONS(選項):查找適用於一個特定網址資源的通訊選擇。 在不需執行具體的涉及數據傳輸的動作情況下, 允許客戶端來確定與資源相關的選項以及 / 或者要求, 或是一個伺服器的性能          registry.antMatchers(HttpMethod.OPTIONS, "/**").denyAll();          // 自動登錄 - cookie儲存方式          registry.and().rememberMe();          // 其餘所有請求都需要認證          registry.anyRequest().authenticated();          // 防止iframe 造成跨域          registry.and().headers().frameOptions().disable();            // 自定義過濾器認證用戶名密碼          http.addFilterAt(adminAuthenticationProcessingFilter, UsernamePasswordAuthenticationFilter.class);      }  }

2、自定義用戶密碼校驗過濾器

@Slf4j  @Component  public class AdminAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {        /**       * @param authenticationManager:             認證管理器       * @param adminAuthenticationSuccessHandler: 認證成功處理       * @param adminAuthenticationFailureHandler: 認證失敗處理       */      public AdminAuthenticationProcessingFilter(CusAuthenticationManager authenticationManager, AdminAuthenticationSuccessHandler adminAuthenticationSuccessHandler, AdminAuthenticationFailureHandler adminAuthenticationFailureHandler) {          super(new AntPathRequestMatcher("/login", "POST"));          this.setAuthenticationManager(authenticationManager);          this.setAuthenticationSuccessHandler(adminAuthenticationSuccessHandler);          this.setAuthenticationFailureHandler(adminAuthenticationFailureHandler);      }        @Override      public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {          if (request.getContentType() == null || !request.getContentType().contains(Constants.REQUEST_HEADERS_CONTENT_TYPE)) {              throw new AuthenticationServiceException("請求頭類型不支援: " + request.getContentType());          }            UsernamePasswordAuthenticationToken authRequest;          try {              MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(request);              // 將前端傳遞的數據轉換成jsonBean數據格式              User user = JSONObject.parseObject(wrappedRequest.getBodyJsonStrByJson(wrappedRequest), User.class);              authRequest = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), null);              authRequest.setDetails(authenticationDetailsSource.buildDetails(wrappedRequest));          } catch (Exception e) {              throw new AuthenticationServiceException(e.getMessage());          }          return this.getAuthenticationManager().authenticate(authRequest);      }  }

3、自定義認證管理器

@Component  public class CusAuthenticationManager implements AuthenticationManager {        private final AdminAuthenticationProvider adminAuthenticationProvider;        public CusAuthenticationManager(AdminAuthenticationProvider adminAuthenticationProvider) {          this.adminAuthenticationProvider = adminAuthenticationProvider;      }        @Override      public Authentication authenticate(Authentication authentication) throws AuthenticationException {          Authentication result = adminAuthenticationProvider.authenticate(authentication);          if (Objects.nonNull(result)) {              return result;          }          throw new ProviderNotFoundException("Authentication failed!");      }  }

4、自定義認證處理

這裡的密碼加密驗證工具類PasswordUtils可在文末源碼中查看

@Component  public class AdminAuthenticationProvider implements AuthenticationProvider {        @Autowired      UserDetailsServiceImpl userDetailsService;      @Autowired      private UserMapper userMapper;        @Override      public Authentication authenticate(Authentication authentication) throws AuthenticationException {          // 獲取前端表單中輸入後返回的用戶名、密碼          String userName = (String) authentication.getPrincipal();          String password = (String) authentication.getCredentials();            SecurityUser userInfo = (SecurityUser) userDetailsService.loadUserByUsername(userName);            boolean isValid = PasswordUtils.isValidPassword(password, userInfo.getPassword(), userInfo.getCurrentUserInfo().getSalt());          // 驗證密碼          if (!isValid) {              throw new BadCredentialsException("密碼錯誤!");          }            // 前後端分離情況下 處理邏輯...          // 更新登錄令牌 - 之後訪問系統其它介面直接通過token認證用戶許可權...          String token = PasswordUtils.encodePassword(System.currentTimeMillis() + userInfo.getCurrentUserInfo().getSalt(), userInfo.getCurrentUserInfo().getSalt());          User user = userMapper.selectById(userInfo.getCurrentUserInfo().getId());          user.setToken(token);          userMapper.updateById(user);          userInfo.getCurrentUserInfo().setToken(token);          return new UsernamePasswordAuthenticationToken(userInfo, password, userInfo.getAuthorities());      }        @Override      public boolean supports(Class<?> aClass) {          return true;      }  }

其中小編自定義了一個UserDetailsServiceImpl類去實現UserDetailsService類 -> 用於認證用戶詳情
和自定義一個SecurityUser類實現UserDetails類 -> 安全認證用戶詳情資訊

@Service("userDetailsService")  public class UserDetailsServiceImpl implements UserDetailsService {        @Autowired      private UserMapper userMapper;        /***       * 根據帳號獲取用戶資訊       * @param username:       * @return: org.springframework.security.core.userdetails.UserDetails       */      @Override      public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {          // 從資料庫中取出用戶資訊          List<User> userList = userMapper.selectList(new EntityWrapper<User>().eq("username", username));          User user;          // 判斷用戶是否存在          if (!CollectionUtils.isEmpty(userList)){              user = userList.get(0);          } else {              throw new UsernameNotFoundException("用戶名不存在!");          }          // 返回UserDetails實現類          return new SecurityUser(user);      }  }

安全認證用戶詳情資訊

@Data  @Slf4j  public class SecurityUser implements UserDetails {      /**       * 當前登錄用戶       */      private transient User currentUserInfo;        public SecurityUser() {      }        public SecurityUser(User user) {          if (user != null) {              this.currentUserInfo = user;          }      }        @Override      public Collection<? extends GrantedAuthority> getAuthorities() {          Collection<GrantedAuthority> authorities = new ArrayList<>();          SimpleGrantedAuthority authority = new SimpleGrantedAuthority("admin");          authorities.add(authority);          return authorities;      }        @Override      public String getPassword() {          return currentUserInfo.getPassword();      }        @Override      public String getUsername() {          return currentUserInfo.getUsername();      }        @Override      public boolean isAccountNonExpired() {          return true;      }        @Override      public boolean isAccountNonLocked() {          return true;      }        @Override      public boolean isCredentialsNonExpired() {          return true;      }        @Override      public boolean isEnabled() {          return true;      }  }

5、自定義認證成功或失敗處理方式

  1. 認證成功處理類實現AuthenticationSuccessHandler類重寫onAuthenticationSuccess方法
  2. 認證失敗處理類實現AuthenticationFailureHandler類重寫onAuthenticationFailure方法

在前後端分離情況下小編認證成功和失敗都返回json數據格式

認證成功後這裡小編只返回了一個token給前端,其它資訊可根據個人業務實際處理

@Component  public class AdminAuthenticationSuccessHandler implements AuthenticationSuccessHandler {      @Override      public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse response, Authentication auth) throws IOException, ServletException {          User user = new User();          SecurityUser securityUser = ((SecurityUser) auth.getPrincipal());          user.setToken(securityUser.getCurrentUserInfo().getToken());          ResponseUtils.out(response, ApiResult.ok("登錄成功!", user));      }  }

認證失敗捕捉異常自定義錯誤資訊返回給前端

@Slf4j  @Component  public class AdminAuthenticationFailureHandler implements AuthenticationFailureHandler {      @Override      public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {          ApiResult result;          if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {              result = ApiResult.fail(e.getMessage());          } else if (e instanceof LockedException) {              result = ApiResult.fail("賬戶被鎖定,請聯繫管理員!");          } else if (e instanceof CredentialsExpiredException) {              result = ApiResult.fail("證書過期,請聯繫管理員!");          } else if (e instanceof AccountExpiredException) {              result = ApiResult.fail("賬戶過期,請聯繫管理員!");          } else if (e instanceof DisabledException) {              result = ApiResult.fail("賬戶被禁用,請聯繫管理員!");          } else {              log.error("登錄失敗:", e);              result = ApiResult.fail("登錄失敗!");          }          ResponseUtils.out(response, result);      }  }
溫馨小提示:

前後端一體的情況下可通過在Spring Security核心配置類中配置異常處理介面然後通過如下方式獲取異常資訊

AuthenticationException e = (AuthenticationException) request.getSession().getAttribute("SPRING_SECURITY_LAST_EXCEPTION");  System.out.println(e.getMessage());

三、前端頁面

這裡2個簡單的html頁面模擬前後端分離情況下登陸處理場景

1、登陸頁

login.html

<!DOCTYPE html>  <html lang="en">  <head>      <meta charset="UTF-8">      <title>Login</title>  </head>  <body>  <h1>Spring Security</h1>  <form method="post" action="" onsubmit="return false">      <div>          用戶名:<input type="text" name="username" id="username">      </div>      <div>          密碼:<input type="password" name="password" id="password">      </div>      <div>  <!--        <label><input type="checkbox" name="remember-me" id="remember-me"/>自動登錄</label>-->          <button onclick="login()">登陸</button>      </div>  </form>  </body>  <script src="http://libs.baidu.com/jquery/1.9.0/jquery.js" type="text/javascript"></script>  <script type="text/javascript">      function login() {          var username = document.getElementById("username").value;          var password = document.getElementById("password").value;          // var rememberMe = document.getElementById("remember-me").value;          $.ajax({              async: false,              type: "POST",              dataType: "json",              url: '/login',              contentType: "application/json",              data: JSON.stringify({                  "username": username,                  "password": password                  // "remember-me": rememberMe              }),              success: function (result) {                  console.log(result)                  if (result.code == 200) {                      alert("登陸成功");                      window.location.href = "../home.html";                  } else {                      alert(result.message)                  }              }          });      }  </script>  </html>
2、首頁

home.html

<!DOCTYPE html>  <html lang="en">  <head>      <meta charset="UTF-8">      <title>Title</title>  </head>  <body>  <h3>您好,登陸成功</h3>  <button onclick="window.location.href='/logout'">退出登錄</button>  </body>  </html>

四、測試介面

@Slf4j  @RestController  public class IndexController {        @GetMapping("/")      public ModelAndView showHome() {          return new ModelAndView("home.html");      }        @GetMapping("/index")      public String index() {          return "Hello World ~";      }        @GetMapping("/login")      public ModelAndView login() {          return new ModelAndView("login.html");      }        @GetMapping("/home")      public String home() {          String name = SecurityContextHolder.getContext().getAuthentication().getName();          log.info("登陸人:" + name);          return "Hello~ " + name;      }        @GetMapping(value ="/admin")      // 訪問路徑`/admin` 具有`crud`許可權      @PreAuthorize("hasPermission('/admin','crud')")      public String admin() {          return "Hello~ 管理員";      }        @GetMapping("/test")  //    @PreAuthorize("hasPermission('/test','t')")      public String test() {          return "Hello~ 測試許可權訪問介面";      }        /**       * 登錄異常處理 - 前後端一體的情況下       * @param request       * @param response       */      @RequestMapping("/login/error")      public void loginError(HttpServletRequest request, HttpServletResponse response) {          AuthenticationException e = (AuthenticationException) request.getSession().getAttribute("SPRING_SECURITY_LAST_EXCEPTION");          log.error(e.getMessage());          ResponseUtils.out(response, ApiResult.fail(e.getMessage()));      }  }

五、測試訪問效果

資料庫帳號:admin 密碼:123456

1. 輸入錯誤用戶名提示該用戶不存在

在這裡插入圖片描述

2. 輸入錯誤密碼提示密碼錯誤

在這裡插入圖片描述

3. 輸入正確用戶名和帳號,提示登陸成功,然後跳轉到首頁

在這裡插入圖片描述
在這裡插入圖片描述

登陸成功後即可正常訪問其他介面,如果是未登錄情況下將訪問不了

在這裡插入圖片描述

溫馨小提示:這裡在未登錄時或訪問未授權的介面時,後端暫時沒有做處理,相關案例將會放在後面的許可權控制案例教程中講解

在這裡插入圖片描述

六、總結

  1. Spring Security核心配置類中設置自定義的用戶密碼校驗過濾器(AdminAuthenticationProcessingFilter)
  2. 在自定義的用戶密碼校驗過濾器中配置認證管理器(CusAuthenticationManager)認證成功處理(AdminAuthenticationSuccessHandler)認證失敗處理(AdminAuthenticationFailureHandler)
  3. 在自定義的認證管理器中配置自定義的認證處理(AdminAuthenticationProvider)
  4. 然後就是在認證處理中實現自己的相應業務邏輯等
Security相關程式碼結構:

在這裡插入圖片描述

本文案例源碼

https://gitee.com/zhengqingya/java-workspace