javascript
自定义request_Spring Security 自定义登录认证(二)
一、前言
本篇文章將講述Spring Security自定義登錄認證校驗用戶名、密碼,自定義密碼加密方式,以及在前后端分離的情況下認證失敗或成功處理返回json格式數據
溫馨小提示:Spring Security中有默認的密碼加密方式以及登錄用戶認證校驗,但小編這里選擇自定義是為了方便以后業務擴展,比如系統默認帶一個超級管理員,當認證時識別到是超級管理員賬號登錄訪問時給它賦予最高權限,可以訪問系統所有api接口,或在登錄認證成功后存入token以便用戶訪問系統其它接口時通過token認證用戶權限等
Spring Security入門學習可參考之前文章:
SpringBoot集成Spring Security入門體驗(一)
二、Spring Security 自定義登錄認證處理
基本環境
數據庫用戶信息表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*/@Overrideprotected 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);}@Overridepublic 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;}@Overridepublic 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 {@AutowiredUserDetailsServiceImpl userDetailsService;@Autowiredprivate UserMapper userMapper;@Overridepublic 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());}@Overridepublic boolean supports(Class<?> aClass) {return true;} }其中小編自定義了一個UserDetailsServiceImpl類去實現UserDetailsService類 -> 用于認證用戶詳情 和自定義一個SecurityUser類實現UserDetails類 -> 安全認證用戶詳情信息
@Service("userDetailsService") public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;/**** 根據賬號獲取用戶信息* @param username:* @return: org.springframework.security.core.userdetails.UserDetails*/@Overridepublic 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;}}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {Collection<GrantedAuthority> authorities = new ArrayList<>();SimpleGrantedAuthority authority = new SimpleGrantedAuthority("admin");authorities.add(authority);return authorities;}@Overridepublic String getPassword() {return currentUserInfo.getPassword();}@Overridepublic String getUsername() {return currentUserInfo.getUsername();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;} }5、自定義認證成功或失敗處理方式
認證成功后這里小編只返回了一個token給前端,其它信息可根據個人業務實際處理
@Component public class AdminAuthenticationSuccessHandler implements AuthenticationSuccessHandler {@Overridepublic 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 {@Overridepublic 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. 輸入正確用戶名和賬號,提示登陸成功,然后跳轉到首頁
登陸成功后即可正常訪問其他接口,如果是未登錄情況下將訪問不了
溫馨小提示:這里在未登錄時或訪問未授權的接口時,后端暫時沒有做處理,相關案例將會放在后面的權限控制案例教程中講解六、總結
Security相關代碼結構:
本文案例源碼
https://gitee.com/zhengqingya/java-workspace
總結
以上是生活随笔為你收集整理的自定义request_Spring Security 自定义登录认证(二)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vtuber面部捕捉工具_泰国程序员开发
- 下一篇: 四面体的表面积_JACS:武汉大学汪成课