Spring Security(2) 自定义登录认证

心已赠人 2023-06-07 08:27 70阅读 0赞

一、前言

本篇文章将讲述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 核心配置类

配置用户密码校验过滤器

  1. @Configuration
  2. @EnableWebSecurity
  3. @EnableGlobalMethodSecurity(prePostEnabled = true)
  4. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  5. /** * 用户密码校验过滤器 */
  6. private final AdminAuthenticationProcessingFilter adminAuthenticationProcessingFilter;
  7. public SecurityConfig(AdminAuthenticationProcessingFilter adminAuthenticationProcessingFilter) {
  8. this.adminAuthenticationProcessingFilter = adminAuthenticationProcessingFilter;
  9. }
  10. /** * 权限配置 * @param http * @throws Exception */
  11. @Override
  12. protected void configure(HttpSecurity http) throws Exception {
  13. ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.antMatcher("/**").authorizeRequests();
  14. // 禁用CSRF 开启跨域
  15. http.csrf().disable().cors();
  16. // 登录处理 - 前后端一体的情况下
  17. // registry.and().formLogin().loginPage("/login").defaultSuccessUrl("/").permitAll()
  18. // // 自定义登陆用户名和密码属性名,默认为 username和password
  19. // .usernameParameter("username").passwordParameter("password")
  20. // // 异常处理
  21. // .failureUrl("/login/error").permitAll()
  22. // // 退出登录
  23. // .and().logout().permitAll();
  24. // 标识只能在 服务器本地ip[127.0.0.1或localhost] 访问`/home`接口,其他ip地址无法访问
  25. registry.antMatchers("/home").hasIpAddress("127.0.0.1");
  26. // 允许匿名的url - 可理解为放行接口 - 多个接口使用,分割
  27. registry.antMatchers("/login", "/index").permitAll();
  28. // OPTIONS(选项):查找适用于一个特定网址资源的通讯选择。 在不需执行具体的涉及数据传输的动作情况下, 允许客户端来确定与资源相关的选项以及 / 或者要求, 或是一个服务器的性能
  29. registry.antMatchers(HttpMethod.OPTIONS, "/**").denyAll();
  30. // 自动登录 - cookie储存方式
  31. registry.and().rememberMe();
  32. // 其余所有请求都需要认证
  33. registry.anyRequest().authenticated();
  34. // 防止iframe 造成跨域
  35. registry.and().headers().frameOptions().disable();
  36. // 自定义过滤器认证用户名密码
  37. http.addFilterAt(adminAuthenticationProcessingFilter, UsernamePasswordAuthenticationFilter.class);
  38. }
  39. }

2、自定义用户密码校验过滤器

  1. @Slf4j
  2. @Component
  3. public class AdminAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
  4. /** * @param authenticationManager: 认证管理器 * @param adminAuthenticationSuccessHandler: 认证成功处理 * @param adminAuthenticationFailureHandler: 认证失败处理 */
  5. public AdminAuthenticationProcessingFilter(CusAuthenticationManager authenticationManager, AdminAuthenticationSuccessHandler adminAuthenticationSuccessHandler, AdminAuthenticationFailureHandler adminAuthenticationFailureHandler) {
  6. super(new AntPathRequestMatcher("/login", "POST"));
  7. this.setAuthenticationManager(authenticationManager);
  8. this.setAuthenticationSuccessHandler(adminAuthenticationSuccessHandler);
  9. this.setAuthenticationFailureHandler(adminAuthenticationFailureHandler);
  10. }
  11. @Override
  12. public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
  13. if (request.getContentType() == null || !request.getContentType().contains(Constants.REQUEST_HEADERS_CONTENT_TYPE)) {
  14. throw new AuthenticationServiceException("请求头类型不支持: " + request.getContentType());
  15. }
  16. UsernamePasswordAuthenticationToken authRequest;
  17. try {
  18. MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(request);
  19. // 将前端传递的数据转换成jsonBean数据格式
  20. User user = JSONObject.parseObject(wrappedRequest.getBodyJsonStrByJson(wrappedRequest), User.class);
  21. authRequest = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), null);
  22. authRequest.setDetails(authenticationDetailsSource.buildDetails(wrappedRequest));
  23. } catch (Exception e) {
  24. throw new AuthenticationServiceException(e.getMessage());
  25. }
  26. return this.getAuthenticationManager().authenticate(authRequest);
  27. }
  28. }

3、自定义认证管理器

  1. @Component
  2. public class CusAuthenticationManager implements AuthenticationManager {
  3. private final AdminAuthenticationProvider adminAuthenticationProvider;
  4. public CusAuthenticationManager(AdminAuthenticationProvider adminAuthenticationProvider) {
  5. this.adminAuthenticationProvider = adminAuthenticationProvider;
  6. }
  7. @Override
  8. public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  9. Authentication result = adminAuthenticationProvider.authenticate(authentication);
  10. if (Objects.nonNull(result)) {
  11. return result;
  12. }
  13. throw new ProviderNotFoundException("Authentication failed!");
  14. }
  15. }

4、自定义认证处理

这里的密码加密验证工具类PasswordUtils可在文末源码中查看

  1. @Component
  2. public class AdminAuthenticationProvider implements AuthenticationProvider {
  3. @Autowired
  4. UserDetailsServiceImpl userDetailsService;
  5. @Autowired
  6. private UserMapper userMapper;
  7. @Override
  8. public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  9. // 获取前端表单中输入后返回的用户名、密码
  10. String userName = (String) authentication.getPrincipal();
  11. String password = (String) authentication.getCredentials();
  12. SecurityUser userInfo = (SecurityUser) userDetailsService.loadUserByUsername(userName);
  13. boolean isValid = PasswordUtils.isValidPassword(password, userInfo.getPassword(), userInfo.getCurrentUserInfo().getSalt());
  14. // 验证密码
  15. if (!isValid) {
  16. throw new BadCredentialsException("密码错误!");
  17. }
  18. // 前后端分离情况下 处理逻辑...
  19. // 更新登录令牌 - 之后访问系统其它接口直接通过token认证用户权限...
  20. String token = PasswordUtils.encodePassword(System.currentTimeMillis() + userInfo.getCurrentUserInfo().getSalt(), userInfo.getCurrentUserInfo().getSalt());
  21. User user = userMapper.selectById(userInfo.getCurrentUserInfo().getId());
  22. user.setToken(token);
  23. userMapper.updateById(user);
  24. userInfo.getCurrentUserInfo().setToken(token);
  25. return new UsernamePasswordAuthenticationToken(userInfo, password, userInfo.getAuthorities());
  26. }
  27. @Override
  28. public boolean supports(Class<?> aClass) {
  29. return true;
  30. }
  31. }

其中小编自定义了一个UserDetailsServiceImpl类去实现UserDetailsService类 -> 用于认证用户详情
和自定义一个SecurityUser类实现UserDetails类 -> 安全认证用户详情信息

  1. @Service("userDetailsService")
  2. public class UserDetailsServiceImpl implements UserDetailsService {
  3. @Autowired
  4. private UserMapper userMapper;
  5. /*** * 根据账号获取用户信息 * @param username: * @return: org.springframework.security.core.userdetails.UserDetails */
  6. @Override
  7. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  8. // 从数据库中取出用户信息
  9. List<User> userList = userMapper.selectList(new EntityWrapper<User>().eq("username", username));
  10. User user;
  11. // 判断用户是否存在
  12. if (!CollectionUtils.isEmpty(userList)){
  13. user = userList.get(0);
  14. } else {
  15. throw new UsernameNotFoundException("用户名不存在!");
  16. }
  17. // 返回UserDetails实现类
  18. return new SecurityUser(user);
  19. }
  20. }

安全认证用户详情信息

  1. @Data
  2. @Slf4j
  3. public class SecurityUser implements UserDetails {
  4. /** * 当前登录用户 */
  5. private transient User currentUserInfo;
  6. public SecurityUser() {
  7. }
  8. public SecurityUser(User user) {
  9. if (user != null) {
  10. this.currentUserInfo = user;
  11. }
  12. }
  13. @Override
  14. public Collection<? extends GrantedAuthority> getAuthorities() {
  15. Collection<GrantedAuthority> authorities = new ArrayList<>();
  16. SimpleGrantedAuthority authority = new SimpleGrantedAuthority("admin");
  17. authorities.add(authority);
  18. return authorities;
  19. }
  20. @Override
  21. public String getPassword() {
  22. return currentUserInfo.getPassword();
  23. }
  24. @Override
  25. public String getUsername() {
  26. return currentUserInfo.getUsername();
  27. }
  28. @Override
  29. public boolean isAccountNonExpired() {
  30. return true;
  31. }
  32. @Override
  33. public boolean isAccountNonLocked() {
  34. return true;
  35. }
  36. @Override
  37. public boolean isCredentialsNonExpired() {
  38. return true;
  39. }
  40. @Override
  41. public boolean isEnabled() {
  42. return true;
  43. }
  44. }

5、自定义认证成功或失败处理方式

  1. 认证成功处理类实现AuthenticationSuccessHandler类重写onAuthenticationSuccess方法
  2. 认证失败处理类实现AuthenticationFailureHandler类重写onAuthenticationFailure方法

在前后端分离情况下小编认证成功和失败都返回json数据格式

认证成功后这里小编只返回了一个token给前端,其它信息可根据个人业务实际处理

  1. @Component
  2. public class AdminAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
  3. @Override
  4. public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse response, Authentication auth) throws IOException, ServletException {
  5. User user = new User();
  6. SecurityUser securityUser = ((SecurityUser) auth.getPrincipal());
  7. user.setToken(securityUser.getCurrentUserInfo().getToken());
  8. ResponseUtils.out(response, ApiResult.ok("登录成功!", user));
  9. }
  10. }

认证失败捕捉异常自定义错误信息返回给前端

  1. @Slf4j
  2. @Component
  3. public class AdminAuthenticationFailureHandler implements AuthenticationFailureHandler {
  4. @Override
  5. public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
  6. ApiResult result;
  7. if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
  8. result = ApiResult.fail(e.getMessage());
  9. } else if (e instanceof LockedException) {
  10. result = ApiResult.fail("账户被锁定,请联系管理员!");
  11. } else if (e instanceof CredentialsExpiredException) {
  12. result = ApiResult.fail("证书过期,请联系管理员!");
  13. } else if (e instanceof AccountExpiredException) {
  14. result = ApiResult.fail("账户过期,请联系管理员!");
  15. } else if (e instanceof DisabledException) {
  16. result = ApiResult.fail("账户被禁用,请联系管理员!");
  17. } else {
  18. log.error("登录失败:", e);
  19. result = ApiResult.fail("登录失败!");
  20. }
  21. ResponseUtils.out(response, result);
  22. }
  23. }
温馨小提示:

前后端一体的情况下可通过在Spring Security核心配置类中配置异常处理接口然后通过如下方式获取异常信息

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

三、前端页面

这里2个简单的html页面模拟前后端分离情况下登陆处理场景

1、登陆页

login.html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Login</title>
  6. </head>
  7. <body>
  8. <h1>Spring Security</h1>
  9. <form method="post" action="" onsubmit="return false">
  10. <div>
  11. 用户名:<input type="text" name="username" id="username">
  12. </div>
  13. <div>
  14. 密码:<input type="password" name="password" id="password">
  15. </div>
  16. <div>
  17. <!-- <label><input type="checkbox" name="remember-me" id="remember-me"/>自动登录</label>-->
  18. <button onclick="login()">登陆</button>
  19. </div>
  20. </form>
  21. </body>
  22. <script src="http://libs.baidu.com/jquery/1.9.0/jquery.js" type="text/javascript"></script>
  23. <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>
  24. </html>
2、首页

home.html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. </head>
  7. <body>
  8. <h3>您好,登陆成功</h3>
  9. <button onclick="window.location.href='/logout'">退出登录</button>
  10. </body>
  11. </html>

四、测试接口

  1. @Slf4j
  2. @RestController
  3. public class IndexController {
  4. @GetMapping("/")
  5. public ModelAndView showHome() {
  6. return new ModelAndView("home.html");
  7. }
  8. @GetMapping("/index")
  9. public String index() {
  10. return "Hello World ~";
  11. }
  12. @GetMapping("/login")
  13. public ModelAndView login() {
  14. return new ModelAndView("login.html");
  15. }
  16. @GetMapping("/home")
  17. public String home() {
  18. String name = SecurityContextHolder.getContext().getAuthentication().getName();
  19. log.info("登陆人:" + name);
  20. return "Hello~ " + name;
  21. }
  22. @GetMapping(value ="/admin")
  23. // 访问路径`/admin` 具有`crud`权限
  24. @PreAuthorize("hasPermission('/admin','crud')")
  25. public String admin() {
  26. return "Hello~ 管理员";
  27. }
  28. @GetMapping("/test")
  29. // @PreAuthorize("hasPermission('/test','t')")
  30. public String test() {
  31. return "Hello~ 测试权限访问接口";
  32. }
  33. /** * 登录异常处理 - 前后端一体的情况下 * @param request * @param response */
  34. @RequestMapping("/login/error")
  35. public void loginError(HttpServletRequest request, HttpServletResponse response) {
  36. AuthenticationException e = (AuthenticationException) request.getSession().getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
  37. log.error(e.getMessage());
  38. ResponseUtils.out(response, ApiResult.fail(e.getMessage()));
  39. }
  40. }

五、测试访问效果

数据库账号:admin 密码:123456

1. 输入错误用户名提示该用户不存在

在这里插入图片描述

2. 输入错误密码提示密码错误

在这里插入图片描述

3. 输入正确用户名和账号,提示登陆成功,然后跳转到首页

在这里插入图片描述
在这里插入图片描述

登陆成功后即可正常访问其他接口,如果是未登录情况下将访问不了

在这里插入图片描述

温馨小提示:这里在未登录时或访问未授权的接口时,后端暂时没有做处理,相关案例将会放在后面的权限控制案例教程中讲解

在这里插入图片描述

六、总结

  1. Spring Security核心配置类中设置自定义的用户密码校验过滤器(AdminAuthenticationProcessingFilter)
  2. 在自定义的用户密码校验过滤器中配置认证管理器(CusAuthenticationManager)认证成功处理(AdminAuthenticationSuccessHandler)认证失败处理(AdminAuthenticationFailureHandler)
  3. 在自定义的认证管理器中配置自定义的认证处理(AdminAuthenticationProvider)
  4. 然后就是在认证处理中实现自己的相应业务逻辑等
Security相关代码结构:

在这里插入图片描述

本文案例源码

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

发表评论

表情:
评论列表 (有 0 条评论,70人围观)

还没有评论,来说两句吧...

相关阅读