Spring Security 自定義登錄認證(二)
- 2019 年 10 月 14 日
- 筆記
一、前言
本篇文章將講述Spring Security自定義登錄認證校驗用戶名、密碼,自定義密碼加密方式,以及在前後端分離的情況下認證失敗或成功處理返回json格式數據
溫馨小提示:Spring Security中有默認的密碼加密方式以及登錄用戶認證校驗,但小編這裡選擇自定義是為了方便以後業務擴展,比如系統默認帶一個超級管理員,當認證時識別到是超級管理員帳號登錄訪問時給它賦予最高許可權,可以訪問系統所有api介面,或在登錄認證成功後存入token以便用戶訪問系統其它介面時通過token認證用戶許可權等
Spring Security入門學習可參考之前文章:
SpringBoot集成Spring Security入門體驗(一)
二、Spring Security 自定義登錄認證處理
基本環境
- spring-boot 2.1.8
- mybatis-plus 2.2.0
- mysql
- 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、自定義認證成功或失敗處理方式
- 認證成功處理類實現
AuthenticationSuccessHandler
類重寫onAuthenticationSuccess
方法 - 認證失敗處理類實現
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. 輸入正確用戶名和帳號,提示登陸成功,然後跳轉到首頁
登陸成功後即可正常訪問其他介面,如果是未登錄情況下將訪問不了
溫馨小提示:這裡在未登錄時或訪問未授權的介面時,後端暫時沒有做處理,相關案例將會放在後面的許可權控制案例教程中講解
六、總結
- 在
Spring Security核心配置類
中設置自定義的用戶密碼校驗過濾器(AdminAuthenticationProcessingFilter)
- 在自定義的用戶密碼校驗過濾器中配置
認證管理器(CusAuthenticationManager)
、認證成功處理(AdminAuthenticationSuccessHandler)
和認證失敗處理(AdminAuthenticationFailureHandler)
等 - 在自定義的認證管理器中配置自定義的
認證處理(AdminAuthenticationProvider)
- 然後就是在認證處理中實現自己的相應業務邏輯等
Security相關程式碼結構:
本文案例源碼
https://gitee.com/zhengqingya/java-workspace