# LWH11 **Repository Path**: lin8081/LWH11 ## Basic Information - **Project Name**: LWH11 - **Description**: SpringBoot整合SpringSecurity之自动登录及异常处理 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-03-13 - **Last Updated**: 2020-12-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## SpringBoot整合SpringSecurity二 在上一章[入门案例]() 中,我们实现了入门程序,本篇我们在上一章的基础上完成自动登录功能及异常处理。 ## 1.自动登录 所谓自动登陆就是当用户第一次访问网站时,输入用户名和密码,然后勾选了自动登陆复选框,进入首页后,点击退出登陆,关闭网页,再次打开同样的网站,则无需再次输入账号密码,直接进入首页,这种交互方式就是“自动登录”。 ### 1.修改登录页面:login.html ```html
``` ### 2.自动登录的实现方式---数据库存储 使用 Cookie 存储虽然方便,但是 Cookie 毕竟是保存在客户端的,而且 Cookie 的值还与用户名、密码这些敏感信息有关,虽然加密了,但是将这些敏感信息存在客户端,毕竟不太保险。 **SpringSecurity 还提供了另一种相对安全的实现机制: ** - 在客户端的 Cookie中,仅保存一个无意义的加密串(与用户名和密码等敏感信息无关),然后在数据库中保存该加密串 - 用户信息的对应关系,自动登录时,用 Cookie 中的加密串,到数据库验证,如果通过,自动登录才算成功。 #### 1.基本原理 当浏览器发起表单登录请求时,当通过 `UsernamePasswordAuthenticationFilter` 认证成功后,会经过 `RememberMeService`, 在其中有个 `TokenRepository` , 它会生成一个 `token`, 首先将 token 写入到浏览器的 `Cookie `中,然后将 token、认证成功的用户名写入到数据库中。 当浏览器下次请求时,会经过 `RememberMeAuthenticationFilter`,它会读取 `Cookie` 中的 `token`,交给 `RememberMeService` ,获取用户信息,并将用户信息放入到 `SpringSecurity` 中,实现自动登录。 ![1583765716646](assets/1583765716646.png) `RememberMeAuthenticationFilter `在整个过滤器链中是比较靠后的位置,也就是说在传统的登录方式都无法登录情况下才会使用自动登录。 ![1583765815108](assets/1583765815108.png) ### 3.创建token数据库表 可由以下代码进行创建token表,但数据库表时已经存在,请注释掉,否则会报错 ```sql tokenRepository.setCreateTableOnStartup(true); ``` 也可以直接sql语句创建表结构 ```sql CREATE TABLE `persistent_logins` ( `username` varchar(64) NOT NULL, `series` varchar(64) NOT NULL, `token` varchar(64) NOT NULL, `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`series`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ``` ### 4.代码实现 在 WebSecurityConfig 中注入 dataSource ,创建一个 PersistentTokenRepository 的Bean对象: ```java @Autowired private DataSource dataSource; @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); // 如果token表不存在,使用下面可以自动初始化表结构,如果已经存在,请注释掉,否则会报错 // tokenRepository.setCreateTableOnStartup(true); return tokenRepository; } ``` 在 config() 中配置自动登录: ```java @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 如果有允许匿名的url,填在下面 // .antMatchers().permitAll() .anyRequest().authenticated() .and() // 设置登陆页 .formLogin().loginPage("/login") // 设置登陆成功页 .defaultSuccessUrl("/").permitAll() // 自定义登陆用户名和密码参数,默认为username和password // .usernameParameter("username") // .passwordParameter("password") .and() .logout().permitAll() // 自动登录 .and().rememberMe() .tokenRepository(persistentTokenRepository()) // token有效时间,单位:s .tokenValiditySeconds(60) .userDetailsService(userDetailsService); // 关闭CSRF跨域 http.csrf().disable(); } ``` ### 3.运行测试 勾选自动登录后,Cookie 和数据库中均存储了 token 信息: ![输入图片说明](https://images.gitee.com/uploads/images/2020/0313/154257_4ef70fce_2068068.png "屏幕截图.png") ## 2.异常处理 我们登录失败的时候,SpringSecurity 帮我们跳转到了 `/login?error` URL,奇怪的是不管是控制台还是网页上都没有打印错误信息。 这是因为首先 `/login?error` 是SpringSecurity 默认的失败 URL,其次如果你不自己处理这个异常,这个异常时不会被处理的。 ### 1.常见异常 我们先来列举下一些 SpringSecurity 中常见的异常: - `UsernameNotFoundException` (用户不存在) - `DisableException`(用户已被禁用) - `BadCredentialsException`(坏的凭据) - `LockedException`(账号锁定) - `CerdentialsExpiredException`(证书过期) - ... 以上列出的这些异常都是 `AuthenticationException` 的子类,然后我们看 SpringSecurity 是如何处理 `AuthenticationException` 异常的。 ### 2.源码分析 SpringSecurity的异常处理是在过滤器中进行的,我们在 `AbastrctAuthenticationProcessingFilter` 中找到了对 `Authentication` 的处理: - 在 doFilter() 中,捕获 AuthenticationException 异常,并交给 unsuccessfulAuthentication() 处理。 - 在 `unsuccessfulAuthentication()` 中,转交给了 `SimpleUrlAuthenticationFailureHandler` 类的 `onAuthencicationFailure()` 处理。 - 在 onAuthenticationFailure() 中,首先判断有没有设置 `defaultFailureUrl`。 a. 如果没有设置,直接返回 401 错误,即 `HttpStatus.UNAUTHORIZED` 的值。 b. 如果设置了,首先执行 `saveException()` 方法。然后判断 `forwardToDestination` 是否为服务器调整,默认使用重定向即客户端跳转。 - 在 saveException() 方法中,首先判断 `forwardToDestination`,如果使用服务器跳转则写入`Request`,客户端跳转则写入 `Session`。写入名为 `WebAttributes.AUTHENTICATION_EXCEPTION` 常量对应值`SPRING_SECURITY_LAST_EXCEPTION`,值为 `AuthenticationException` 对象。 - 至此 SpringSecurity 完成了异常处理,总结下流程: –> AbstractAuthenticationProcessingFilter`.doFilter()` –> AbstractAuthenticationProcessingFilter.`unsuccessfulAuthentication()` –> SimpleUrlAuthenticationFailureHandler.`onAuthenticationFailure()` –> SimpleUrlAuthenticationFailureHandler.`saveException()` ### 3.处理异常 上面通过源码看着挺复杂,但真正处理起来SpringSecurity为我们提供了方便的方式,我们只需要指定错误的url,然后在该方法中对异常进行处理即可。 - 指定错误url ,在`WebSecurityConfig` 中添加 `.failureUrl("/login/error")`: ```java @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 如果有允许匿名的url,填在下面 // .antMatchers().permitAll() .anyRequest().authenticated() .and() // 设置登陆页 .formLogin().loginPage("/login") // 设置登陆成功url .defaultSuccessUrl("/").permitAll() // 设置登录失败url .failureUrl("/login/error") // 自定义登陆用户名和密码参数,默认为username和password // .usernameParameter("username") // .passwordParameter("password") .and() .logout().permitAll() // 自动登录 .and().rememberMe() .tokenRepository(persistentTokenRepository()) // 有效时间,单位:s .tokenValiditySeconds(60) .userDetailsService(userDetailsService); // 关闭CSRF跨域 http.csrf().disable(); } ``` - 在 Controller 中编写 `loginError`方法完成异常处理操作: ```java @GetMapping("/login/error") @ResponseBody public Result loginError(HttpServletRequest request) { AuthenticationException authenticationException = (AuthenticationException) request.getSession().getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); log.info("authenticationException={}", authenticationException); Result result = new Result(); result.setCode(201); if (authenticationException instanceof UsernameNotFoundException || authenticationException instanceof BadCredentialsException) { result.setMsg("用户名或密码错误"); } else if (authenticationException instanceof DisabledException) { result.setMsg("用户已被禁用"); } else if (authenticationException instanceof LockedException) { result.setMsg("账户被锁定"); } else if (authenticationException instanceof AccountExpiredException) { result.setMsg("账户过期"); } else if (authenticationException instanceof CredentialsExpiredException) { result.setMsg("证书过期"); } else { result.setMsg("登录失败"); } return result; } ``` 修改 `CustomUserDetailsService` `loadUserByUsername()` 方法的返回值: ```java @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Collection authorities = new ArrayList<>(); // 从数据库中取出用户信息 SysUser user = userService.selectByName(username); // 判断用户是否存在 if(user == null) { throw new UsernameNotFoundException("用户名不存在"); } // 添加权限 List userRoles = userRoleService.listByUserId(user.getId()); for (SysUserRole userRole : userRoles) { SysRole role = roleService.selectById(userRole.getRoleId()); authorities.add(new SimpleGrantedAuthority(role.getName())); } // 返回UserDetails实现类 return new User(user.getUsername(), user.getPassword(),true, true, true, true, authorities); } ``` ### 4.运行项目 账号密码错误情况下: ![输入图片说明](https://images.gitee.com/uploads/images/2020/0313/154228_72d1f965_2068068.png "屏幕截图.png")