SpringBoot整合SpringSecurity二
在上一章入门案例 中,我们实现了入门程序,本篇我们在上一章的基础上完成自动登录功能及异常处理。
本案例源码地址:https://gitee.com/lin8081/LWH11
1.自动登录
所谓自动登陆就是当用户第一次访问网站时,输入用户名和密码,然后勾选了自动登陆复选框,进入首页后,点击退出登陆,关闭网页,再次打开同样的网站,则无需再次输入账号密码,直接进入首页,这种交互方式就是“自动登录”。
1.修改登录页面:login.html
<form method="post" action="/login">
<!-- 账号 -->
<div class="layui-form-item">
<label class="layui-form-label">账号</label>
<div class="layui-input-block">
<input name="username" id="userName" value="admin" placeholder="默认账号:admin" type="text" lay-verify="required" class="layui-input">
</div>
</div>
<!-- 密码 -->
<div class="layui-form-item">
<label class="layui-form-label">密码</label>
<div class="layui-input-block">
<input name="password" id="password" value="123" placeholder="默认密码:123" type="password" lay-verify="required" class="layui-input">
</div>
</div>
<!-- 自动登录 -->
<div class="layui-form-item">
<label><input style="display: inline" type="checkbox" name="remember-me"/>自动登录</label>
</div>
<div>
<button type="submit" class="layui-btn layui-btn-fluid" >登 录</button>
</div>
</form>
2.自动登录的实现方式—数据库存储
使用 Cookie 存储虽然方便,但是 Cookie 毕竟是保存在客户端的,而且 Cookie 的值还与用户名、密码这些敏感信息有关,虽然加密了,但是将这些敏感信息存在客户端,毕竟不太保险。
**SpringSecurity 还提供了另一种相对安全的实现机制: **
- 在客户端的 Cookie中,仅保存一个无意义的加密串(与用户名和密码等敏感信息无关),然后在数据库中保存该加密串 - 用户信息的对应关系,自动登录时,用 Cookie 中的加密串,到数据库验证,如果通过,自动登录才算成功。
1.基本原理
当浏览器发起表单登录请求时,当通过 UsernamePasswordAuthenticationFilter
认证成功后,会经过 RememberMeService
, 在其中有个 TokenRepository
, 它会生成一个 token
, 首先将 token 写入到浏览器的 Cookie
中,然后将 token、认证成功的用户名写入到数据库中。
当浏览器下次请求时,会经过 RememberMeAuthenticationFilter
,它会读取 Cookie
中的 token
,交给 RememberMeService
,获取用户信息,并将用户信息放入到 SpringSecurity
中,实现自动登录。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lk1OUSjB-1584085522201)(assets/1583765716646.png)]
RememberMeAuthenticationFilter
在整个过滤器链中是比较靠后的位置,也就是说在传统的登录方式都无法登录情况下才会使用自动登录。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S4TsyYmA-1584085522204)(assets/1583765815108.png)]
3.创建token数据库表
可由以下代码进行创建token表,但数据库表时已经存在,请注释掉,否则会报错
tokenRepository.setCreateTableOnStartup(true);
也可以直接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对象:
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 如果token表不存在,使用下面可以自动初始化表结构,如果已经存在,请注释掉,否则会报错
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
在 config() 中配置自动登录:
@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 信息:
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")
:
@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
方法完成异常处理操作:@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()
方法的返回值:@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Collection<GrantedAuthority> authorities = new ArrayList<>(); // 从数据库中取出用户信息 SysUser user = userService.selectByName(username); // 判断用户是否存在 if(user == null) { throw new UsernameNotFoundException("用户名不存在"); } // 添加权限 List<SysUserRole> 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.运行项目
账号密码错误情况下: