在上一章入门案例 中,我们实现了入门程序,本篇我们在上一章的基础上完成自动登录功能及异常处理。
所谓自动登陆就是当用户第一次访问网站时,输入用户名和密码,然后勾选了自动登陆复选框,进入首页后,点击退出登陆,关闭网页,再次打开同样的网站,则无需再次输入账号密码,直接进入首页,这种交互方式就是“自动登录”。
<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>
使用 Cookie 存储虽然方便,但是 Cookie 毕竟是保存在客户端的,而且 Cookie 的值还与用户名、密码这些敏感信息有关,虽然加密了,但是将这些敏感信息存在客户端,毕竟不太保险。
**SpringSecurity 还提供了另一种相对安全的实现机制: **
当浏览器发起表单登录请求时,当通过 UsernamePasswordAuthenticationFilter
认证成功后,会经过 RememberMeService
, 在其中有个 TokenRepository
, 它会生成一个 token
, 首先将 token 写入到浏览器的 Cookie
中,然后将 token、认证成功的用户名写入到数据库中。
当浏览器下次请求时,会经过 RememberMeAuthenticationFilter
,它会读取 Cookie
中的 token
,交给 RememberMeService
,获取用户信息,并将用户信息放入到 SpringSecurity
中,实现自动登录。
RememberMeAuthenticationFilter
在整个过滤器链中是比较靠后的位置,也就是说在传统的登录方式都无法登录情况下才会使用自动登录。
可由以下代码进行创建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
在 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();
}
勾选自动登录后,Cookie 和数据库中均存储了 token 信息:
我们登录失败的时候,SpringSecurity 帮我们跳转到了 /login?error
URL,奇怪的是不管是控制台还是网页上都没有打印错误信息。
这是因为首先 /login?error
是SpringSecurity 默认的失败 URL,其次如果你不自己处理这个异常,这个异常时不会被处理的。
我们先来列举下一些 SpringSecurity 中常见的异常:
UsernameNotFoundException
(用户不存在)DisableException
(用户已被禁用)BadCredentialsException
(坏的凭据)LockedException
(账号锁定)CerdentialsExpiredException
(证书过期)AuthenticationException
的子类,然后我们看 SpringSecurity 是如何处理 AuthenticationException
异常的。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()
上面通过源码看着挺复杂,但真正处理起来SpringSecurity为我们提供了方便的方式,我们只需要指定错误的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);
}
账号密码错误情况下:
Sign in to post a comment
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。
Repository Comments ( 0 )