spring security——用户认证流程(二)

短命女 2021-10-13 02:49 860阅读 0赞

一、用户认证逻辑

  1. 在自定义用户认证逻辑中我们需要完成的内容有:
  • 处理用户信息获取逻辑
  • 处理用户校验逻辑
  • 处理密码加密解密

1、处理用户信息获取逻辑(UserDetailsService)

  1. org.springframework.security.core.userdetails.UserDetailsService

UserDetailsService接口用于加载用户特定的数据,它在整个框架中作为用户DAO使用,是验证提供者使用的策略。 该接口只需要一个只读方法,这简化了对新的数据访问策略的支持。实现一个自定义的UserDetailsService

  1. // 自定义数据源来获取数据
  2. // 这里只要是存在一个自定义的 UserDetailsService ,那么security将会使用该实例进行配置
  3. @Component
  4. public class MyUserDetailsService implements UserDetailsService {
  5. Logger logger = LoggerFactory.getLogger(getClass());
  6. // 可以从任何地方获取数据
  7. @Override
  8. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  9. // 根据用户名查找用户信息
  10. logger.info("登录用户名", username);
  11. // 写死一个密码,赋予一个admin权限
  12. return new User(username, "123456",
  13. AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
  14. }
  15. }

这样就能让自定义的UserdetailsService生效了。但是在浏览器中登录的时候,后台报错了

  1. java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

这个异常是spring security5+后密码策略变更了。必须使用 PasswordEncoder 方式,也就是你存储密码的时候需要使用{noop}123456这样的方式。这个在官网文档中有讲到,花括号里面的是encoder id ,这个支持的全部列表在以下的方法中定义

  1. org.springframework.security.crypto.factory.PasswordEncoderFactories#createDelegatingPasswordEncoder

noop 对应的处理类是org.springframework.security.crypto.password.NoOpPasswordEncoder,只用于测试,因为没有做任何加密功能。

2、处理用户校验逻辑(UserDetails)

  1. 自定义的其他逻辑是在 org.springframework.security.core.userdetails.User 中提供的,只要在登录的时候把user中提供的信息返回即可达到支持的业务逻辑,下面列出支持的业务场景:
  • isEnabled 账户是否启用
  • isAccountNonExpired 账户没有过期
  • isCredentialsNonExpired 身份认证是否是有效的
  • isAccountNonLocked 账户没有被锁定

对于 isAccountNonLocked 和 isEnabled 没有做业务处理,只是抛出了对于的异常信息;

  1. // 这里的几个布尔值的含义对应上面列出来的顺序
  2. User admin = new User(username, "{noop}123456",
  3. true, true, true, false,
  4. AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));

3、处理密码加密解密(PasswordEncoder)

  1. 密码加密解密是使用了下面这个类
  2. org.springframework.security.crypto.password.PasswordEncoder

配置只需要提供一个实例即可,会自动使用该实例

  1. @Configuration
  2. public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Bean
  4. public PasswordEncoder passwordEncoder() {
  5. return new BCryptPasswordEncoder();
  6. }

在UserDetailsService中需要模拟存入数据库中的密码就是加密后的字符串

  1. @Component
  2. public class MyUserDetailsService implements UserDetailsService {
  3. Logger logger = LoggerFactory.getLogger(getClass());
  4. @Autowired
  5. private PasswordEncoder passwordEncoder;
  6. @Override
  7. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  8. logger.info("登录用户名:{}", username);
  9. String password = passwordEncoder.encode("123456");
  10. logger.info("数据库密码{}", password);
  11. User admin = new User(username,
  12. // "{noop}123456",
  13. password,
  14. true, true, true, true,
  15. AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
  16. }
  17. }

其框架会把提交的密码使用我们定义的passwordEncode加密后调用org.springframework.security.crypto.password.PasswordEncoder#matches方法,与 返回的User中的密码进行比对。配对正常就验证通过。

二、自定义登录

  1. 上面使用spring security默认的认证流程。处理了自定义的用户数据,密码加密等。但是在实际开发中,肯定是要使用自己开发的页面、登录成功失败的业务处理等。

1、自定义登录页面

  1. 我们在自定义的WebSecurityConfig中修改配置如下:
  2. protected void configure(HttpSecurity http) throws Exception {
  3. http
  4. // 定义表单登录 - 身份认证的方式
  5. .formLogin()
  6. .loginPage("/tin-signIn.html")
  7. .and()
  8. .authorizeRequests()
  9. // 放行这个路径
  10. .antMatchers("/tin-signIn.html").permitAll()
  11. }

如果不对该静态文件放行的话,将会无限跳转,造成错误:”localhost 将您重定向的次数过多”。关于html存放资源文件夹下的哪一个目录,这个有点懵,开始放在static下面访问不到,最后放到resources下面才对(因为我的项目没有整合Thymeleaf,spring boot的默认静态文件路径应该是下面的)

20190821103513878.png

html内容如下

  1. <body>
  2. <h2>标准登录页面</h2>
  3. <form action="/authentication/form" method="post">
  4. <table>
  5. <tr>
  6. <td>用户名:</td>
  7. <td><input type="text" name="username"></td>
  8. </tr>
  9. <tr>
  10. <td>密码:</td>
  11. <td><input type="password" name="password"></td>
  12. </tr>
  13. <tr>
  14. <td colspan="2">
  15. <button type="submit">登录</button>
  16. </td>
  17. </tr>
  18. </table>
  19. </form>
  20. </body>

注意这里的路径:acrion=”/authentication/form”,路径是自定义的,而UsernamePasswordAuthenticationFilter 默认是处理/login路径的登录请求

  1. public UsernamePasswordAuthenticationFilter() {
  2. super(new AntPathRequestMatcher("/login", "POST"));
  3. }

这里我们自定义如下:

  1. .formLogin()
  2. .loginPage("/tin-signIn.html")
  3. // 处理登录请求路径,像是对security默认/login路径的重命名
  4. .loginProcessingUrl("/authentication/form")
  5. .and()
  6. .authorizeRequests()
  7. .antMatchers("/tin-signIn.html").permitAll()
  8. .and()
  9. .csrf().disable();//不加出现_csrf的错误情况,跳回登录页面

2、处理不同类型的请求

  1. 上面虽然配置了自定义的路径,但是都统一跳转到了静态页面,那么要怎么实现 根据请求来分发是返回html内容?还是返回json内容呢?在前后分离的情况下,都用ajax来请求,肯定不能返回html了。

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIyMTcyMTMz_size_16_color_FFFFFF_t_70

思路是上面图中这样。那么很简单只要把登录地址换成自定义的就好了。

  1. .formLogin()
  2. //.loginPage("/tin-signIn.html")
  3. // 更换成自定义的一个真实存在的处理器地址
  4. .loginPage("/authentication/require")

编写处理请求的处理器

  1. @RestController
  2. @Slf4j
  3. public class BrowserSecurityController {
  4. // 封装了引发跳转请求的工具类,看实现类应该是从session中获取的
  5. private RequestCache requestCache = new HttpSessionRequestCache();
  6. // spring的工具类:封装了所有跳转行为策略类
  7. private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
  8. /**
  9. * 当需要身份认证时跳转到这里
  10. */
  11. @RequestMapping("/authentication/require")
  12. @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
  13. public SimpleResponse requirAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
  14. SavedRequest savedRequest = requestCache.getRequest(request, response);
  15. // 如果有引发认证的请求
  16. // spring 在跳转前应该会把信息存放在某个地方?
  17. if (savedRequest != null) {
  18. String targetUrl = savedRequest.getRedirectUrl();
  19. logger.info("引发跳转的请求:" + targetUrl);
  20. // 如果是html请求,则跳转到登录页
  21. if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
  22. redirectStrategy.sendRedirect(request, response, "/tin-signIn.html");
  23. }
  24. }
  25. // 否则都返回需要认证的json串
  26. return new SimpleResponse("访问的服务需要身份认证,请引导用户到登录页");
  27. }
  28. }

SimpleResponse 类只是一个返回结果的信息类。

3、登录成功处理

  1. security 默认的登录成功处理是跳转到需要授权之前访问的url。而在一些场景下:比如前后分离,登录是通过ajax访问,没有办法处理301跳转。而是登录成功则返回相关的数据即可。自定义入口还是在表单登录处配置的
  2. http
  3. // 定义表单登录 - 身份认证的方式
  4. .formLogin()
  5. .loginPage("/authentication/require")
  6. .loginProcessingUrl("/authentication/form")
  7. .successHandler(myAuthenticationSuccessHandler)

myAuthenticationSuccessHandler 的编写如下:

  1. /**
  2. * formLogin().successHandler() 中需要的处理器类型
  3. */
  4. @Component("myAuthenticationSuccessHandler")
  5. public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
  6. private org.slf4j.Logger logger = LoggerFactory.getLogger(getClass());
  7. // spring 是使用jackson来进行处理返回数据的,所以这里可以得到他的实例
  8. @Autowired
  9. private com.fasterxml.jackson.databind.ObjectMapper objectMapper;
  10. /**
  11. * authentication 封装了所有的认证信息
  12. */
  13. @Override
  14. public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
  15. logger.info("登录成功");
  16. response.setContentType("application/json;charset=UTF-8");
  17. response.getWriter().write(objectMapper.writeValueAsString(authentication));
  18. }
  19. }

查看输出的authentication

  1. {
  2. "authorities": [
  3. {
  4. "authority": "admin"
  5. }
  6. ],
  7. "details": {
  8. "remoteAddress": "0:0:0:0:0:0:0:1",
  9. "sessionId": "FE0F33577E7E5D89AF15FCCD6FE5A4B3"
  10. },
  11. "authenticated": true,
  12. "principal": {
  13. "password": null,
  14. "username": "admin",
  15. "authorities": [
  16. {
  17. "authority": "admin"
  18. }
  19. ],
  20. "accountNonExpired": true,
  21. "accountNonLocked": true,
  22. "credentialsNonExpired": true,
  23. "enabled": true
  24. },
  25. "credentials": null,
  26. "name": "admin"
  27. }

如果我们想在ajax访问时返回JSON数据,在普通处理下跳转到之前访问的url该怎么办?

  1. @Component("myAuthenticationSuccessHandler")
  2. public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
  3. private org.slf4j.Logger logger = LoggerFactory.getLogger(getClass());
  4. // com.fasterxml.jackson.databind.
  5. // spring 是使用jackson来进行处理返回数据的
  6. // 所以这里可以得到他的实例
  7. @Autowired
  8. private com.fasterxml.jackson.databind.ObjectMapper objectMapper;
  9. @Autowired
  10. private SecurityProperties securityProperties;
  11. /**
  12. * @param authentication 封装了所有的认证信息
  13. */
  14. @Override
  15. public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
  16. logger.info("登录成功");
  17. if (LoginResponseType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
  18. response.setContentType("application/json;charset=UTF-8");
  19. response.getWriter().write(objectMapper.writeValueAsString(authentication));
  20. } else {
  21. // 把本类实现父类改成 AuthenticationSuccessHandler 的子类 SavedRequestAwareAuthenticationSuccessHandler
  22. // 之前说spring默认成功是跳转到登录前的url地址
  23. // 就是使用的这个类来处理的
  24. super.onAuthenticationSuccess(request, response, authentication);
  25. }
  26. }
  27. }

这样登录成功的就ok了。对于失败的来说是一样的,继承的父类改成spring默认的处理器

  1. org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler

下面简单写一下失败处理器,其实和成功处理器一样

  1. @Component("myAuthenticationFailureHandler")
  2. @Slf4j
  3. public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
  4. @Autowired
  5. private SecurityProperties securityProperties;
  6. @Override
  7. public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
  8. log.info("用户信息校验失败:" + exception.getMessage());
  9. if(LoginResponseType.JSON.equals(securityProperties.getBrowser().getLoginType())){
  10. //response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
  11. response.setContentType("application/json;charset=UTF-8");
  12. response.getWriter().write(JsonUtils.objectToJson(RestResult.build(CodeMsg.USER_LOGIN_FAILURE)));
  13. }else {
  14. super.onAuthenticationFailure(request, response, exception);
  15. }
  16. }
  17. }

三、认证流程详解

  1. 上面讲的都是实现spring给出的扩展钩子。比如:自定义登录页,自定义登录成功处理等。但是是很碎片化的,在脑海中链接不起来,下面我们就来梳理一下用户认证流程的源码。

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIyMTcyMTMz_size_16_color_FFFFFF_t_70 1

下面我们就跟着这个流程和之前配置的地方,去一步一步的看源码

  • AuthenticationManager 管理所有的Provider,并选择适合的进行验证
  • AuthenticationProvider 验证提供者,可以自己写provider处理自己的业务场景逻辑

1、认证结果如何在多个请求之间共享

  1. 在认证成功的情况下,SecurityContext 默策略是一个org.springframework.security.core.context.ThreadLocalSecurityContextHolderStrategy 对象,内部使用\`ThreadLocal<SecurityContext>\`来存储
  2. org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#successfulAuthentication
  3. SecurityContextHolder.getContext().setAuthentication(authResult);

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIyMTcyMTMz_size_16_color_FFFFFF_t_70 2

上面的源码也就是上图右边部分,SecurityContext会在同一个线程中可以通过这个context来获取到的。然后SecurityContextPersistenceFilter 所在的位置如下图

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIyMTcyMTMz_size_16_color_FFFFFF_t_70 3

2、获取认证用户信息

  1. 这里我们在UserController中写获取用户信息的api。在前面分析了源码,所以这里可以使用SecurityContextHolder获取到当前的认证用户信息
  2. @GetMapping("/me")
  3. public Authentication getCurrentUser() {
  4. Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  5. return authentication;
  6. }
  7. // 这样写 spring 会自动注入
  8. @GetMapping("/me")
  9. public Authentication getCurrentUser(Authentication authentication) {
  10. return authentication;
  11. }

输出

  1. {
  2. "authorities": [
  3. {
  4. "authority": "admin"
  5. }
  6. ],
  7. "details": {
  8. "remoteAddress": "0:0:0:0:0:0:0:1",
  9. "sessionId": "CB434B240B0B1810ED922C9D877F5842"
  10. },
  11. "authenticated": true,
  12. "principal": {
  13. "password": null,
  14. "username": "test001",
  15. "authorities": [
  16. {
  17. "authority": "admin"
  18. }
  19. ],
  20. "accountNonExpired": true,
  21. "accountNonLocked": true,
  22. "credentialsNonExpired": true,
  23. "enabled": true
  24. },
  25. "credentials": null,
  26. "name": "test001"
  27. }

注意看,password是不会返回来的!如果上面的信息太多了,也可以使用参数注解@AuthenticationPrincipal UserDetails获取User的信息。比如

  1. public RestResult validatePassword(@AuthenticationPrincipal UserExt user, String password) {
  2. return userService.validatePassword(user, password);
  3. }

四、添加记住我功能

1、基本原理

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIyMTcyMTMz_size_16_color_FFFFFF_t_70 4

  1. security中认证过滤链中的org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter 过滤器来实现的。

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIyMTcyMTMz_size_16_color_FFFFFF_t_70 5

当没有其他的认证过滤器处理的时候,记住我这个过滤器就尝试工作。

2、实现记住我的功能

  1. 首先给默认登录页面增加选项
  2. <!--名称是固定的-->
  3. <input type="checkbox" value="true" name="remember-me">记住我

nam的名称是以下类中被定义

  1. org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer
  2. private static final String DEFAULT_REMEMBER_ME_NAME = "remember-me";

然后在SecurityConfig 中进行配置

  1. public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
  2. // 数据源是需要在使用处配置数据源的信息
  3. @Autowired
  4. private DataSource dataSource;
  5. @Autowired
  6. private PersistentTokenRepository persistentTokenRepository;
  7. // 之前已经写好的 MyUserDetailsService,
  8. @Autowired
  9. private UserDetailsService userDetailsService;
  10. @Bean
  11. public PersistentTokenRepository persistentTokenRepository() {
  12. // org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer.tokenRepository
  13. JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
  14. jdbcTokenRepository.setDataSource(dataSource);
  15. // 该对象里面有定义创建表的语句,可以设置让该类来创建表
  16. // 但是该功能只用使用一次,如果数据库已经存在表则会报错
  17. // jdbcTokenRepository.setCreateTableOnStartup(true);
  18. return jdbcTokenRepository;
  19. }
  20. @Override
  21. protected void configure(HttpSecurity http) throws Exception {
  22. http
  23. // 定义表单登录 - 身份认证的方式
  24. .formLogin()
  25. .loginPage("/authentication/require")
  26. .loginProcessingUrl("/authentication/form")
  27. .and()
  28. // 从这里开始配置记住我的功能
  29. .rememberMe()
  30. .tokenRepository(persistentTokenRepository)
  31. // 新增过期配置,单位秒,默认配置写的60秒
  32. .tokenValiditySeconds(60 * 5)
  33. // userDetailsService 是必须的。不然就报错
  34. .userDetailsService(userDetailsService)

从数据库获取到记住我的token后,验证成功,则通过userDetailsService获取用户信息,然后在框架中写入认证信息,完成登录。

3、测试

  1. 访问/user 肯定被拦截,无权限访问。访问标准登录页:/tin-signIn.html 记住勾选记住我的选项。退出浏览器或则重启系统,直接访问 /user(此时会明显的感觉到系统运行缓慢,这是因为需要从数据库获取信息)。在登录成功的时候会往数据库插入一条数据
  2. INSERT INTO `tin-demo`.`persistent_logins` (`username`, `series`, `token`, `last_used`) VALUES
  3. ('admin', 'eBbVlKvXSseasbH6yVGozQ==', '72sHS9ifyza2yTN3h0Kq7A==', '2018-08-04 14:05:31');

登录一次则会插入一条,那什么时候被删除呢?在源码中看到:

  • 当携带的cookie和数据库中最后一次不匹配的时候,会删除所有与该用户相关的记录
  • 当使用logout功能退出的时候

所以该地方可能会有一些小小的问题,但是可以自己去解决这个数据过多的情况,增加定时任务多长时间清理一次等方法。

4、源码解析

  1. 登录在验证成功之后会调用该方法org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter\#successfulAuthentication。然后委托了org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices\#onLoginSuccess
  2. protected void onLoginSuccess(HttpServletRequest request,
  3. HttpServletResponse response, Authentication successfulAuthentication) {
  4. String username = successfulAuthentication.getName();
  5. logger.debug("Creating new persistent login for user " + username);
  6. PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
  7. username, generateSeriesData(), generateTokenData(), new Date());
  8. try {
  9. tokenRepository.createNewToken(persistentToken);
  10. addCookie(persistentToken, request, response);
  11. }
  12. catch (Exception e) {
  13. logger.error("Failed to save persistent token ", e);
  14. }
  15. }

携带cookie访问的时候会触发这个方法org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices#processAutoLoginCookie,里面有验证过期等的逻辑。

对于看本文比较困难的同学可以移步:https://www.lanqiao.cn/courses/3013

邀请码:STyLDQzM

其实内容和博客差不多,只不过更详细(就是一步一步的来,所有步骤有详细过程记录和截图,按照课程步骤最终能完整的操作完整个项目过程,适合小白),版本上也有所升级,还有就是有问题可以直接沟通,谢谢支持!

发表评论

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

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

相关阅读