1、自动登录原理
大概的流程是这样一个图,里面还有很多细节与类下面进行分析
1.1、首次登录
-
第一次登录时首先需要勾选checkbox的组件,页面中应该给出一个记住我的勾选框!
-
然后Security会放行到AbstractAuthenticationProcessingFilter抽象类,这个类里面doFilter放行链主要调用attemptAuthentication方法、successfulAuthentication方法。
-
其中attemptAuthentication方法由UsernamePasswordAuthenticationFilter类实现,这里会调用自定义的UseDetailsService接口的实现类(用户登录账号密码验证),也就是说这个方法会进行账号密码的校验!
-
successfulAuthentication方法主要是在用户验证通过之后用于Token的生成、存储;其中会用到PersistentTokenBasedRememberMeServices类中的onLoginSuccess方法
-
onLoginSuccess方法核心就是随机生成一个Token、将Token持久化到数据库中、并且将Token写入到Cookie中!
@Override protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { // 1. 登录的用户名账号 String username = successfulAuthentication.getName(); // 2. 生成一个Token PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(), generateTokenData(), new Date()); try { // 3. 持久化到数据库中 this.tokenRepository.createNewToken(persistentToken); // 4. 添加到Cookie中 addCookie(persistentToken, request, response); } catch (Exception ex) { this.logger.error("Failed to save persistent token ", ex); } }
-
这里的Token可以通过代码进行设置过期时间、像什么十天内免登录、三天内免登录…
1.2、自动登录
-
所谓的自动登录是在访问链接时浏览器自动携带上了Cookie中的Token交给后端校验,如果删掉了Cookie或者过期了同样是需要再次验证的!
-
浏览器携带Token进行请求来到RememberMeAuthenticationFilter类中的doFilter方法过滤链中;这里调用AbstractRememberMeServices抽象类中的autoLogin方法。
-
autoLogin方法中会从request中拿到cookie的值,然后调用processAutoLoginCookie方法进行数据库层面的校验!
-
processAutoLoginCookie方法是由PersistentTokenBasedRememberMeServices类给出实现;首先通过Token查到对应的登录账户名。
-
如果匹配失败直接拦截掉请求,否则匹配成功那么重新刷新Token的过期时间并且重新持久化并且写到Cookie中,并且调用自定义的UseDetailsService接口的实现类(用户登录账号密码验证)。
@Override
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
HttpServletResponse response) {
// 1. cookie残缺
if (cookieTokens.length != 2) {
throw new InvalidCookieException("Cookie token did not contain " + 2 + " tokens, but contained '"
+ Arrays.asList(cookieTokens) + "'");
}
String presentedSeries = cookieTokens[0];
String presentedToken = cookieTokens[1];
PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
....
// 2. 这里有一大堆的校验失败
....
// 3. 校验成功,重新生成Cookie等一系列操作
PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(),
generateTokenData(), new Date());
try {
this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
addCookie(newToken, request, response);
}
// 4. 查用户(因为有可能用户删除掉)
return getUserDetailsService().loadUserByUsername(token.getUsername());
}
- 分析:最后为什么要重新查询一次用户?因为Token查询的是表中一个Token + Username的表,并不是用户登录账号表;有可能Token没过期但是删除掉了这个用户,Token中有残余数据!
2、具体实现
前言:首先需要看一下JdbcTokenRepositoryImpl类的源码,这个类的源码里给出了存放token、用户名,时间戳等一系列参数的建表语句;以及操作数据库的语句。
2.1、创建数据表
-
创建数据表可以自己创建,也可以在服务启动时让其自动创建
-
这里选择自动创建表,直接把它的源码复制过来;表名、列名一些参数最好不要动。
// 操作token的数据表
create table persistent_logins (
username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null
)engine=innodb default charset=utf8
// 存放用户信息表
create table `account` (
`id` int(11) not null auto_increment comment '编号',
`username` varchar(30) not null comment '姓名',
`password` varchar(30) not null comment '密码',
`role` varchar(100) not null comment '权限',
primary key (`id`)
) engine=innodb default charset=utf8
insert into account(`id`, `username`, `password`, `role`) values (1, 'admin', '123456', 'root')
2.2、UserDetailsService实现
- 编写Pojo类对应数据库中的表
// pojo
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Integer id;
private String username;
private String password;
private String role;
}
- 编写mapper用户操作数据库的接口
// mapper
@Mapper
@Repository
public interface AccountMapper {
Account getLoginAccount(String username);
}
- 编写mapper用户操作数据库的接口
// accountmapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.splay.mapper.AccountMapper">
<select id="getLoginAccount" parameterType="string" resultType="Account">
select *from account where username = #{username}
</select>
</mapper>
- 编写UserDetailsService接口的实现类,注入Mapper
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
// 注入dao层
@Autowired
AccountMapper mapper;
@Autowired
BCryptPasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
Account account = mapper.getLoginAccount(username);
System.out.println(account.toString());
List<GrantedAuthority> list = new ArrayList<>();
// SpringSecurity权限控制角色需要使用"ROLE_"开头, 并且密码在构造时需要进行加密。
list.add(new SimpleGrantedAuthority("ROLE_" + account.getRole()));
return new User(passwordEncoder.encode(account.getUsername()), passwordEncoder.encode(account.getPassword()), list);
}
}
2.3、Security配置
- Security中首先需要注入BCryptPasswordEncoder加密解密类、数据源DataSource、JdbcTokenRepositoryImpl操作Token的驱动类、以及UserDetailsService类(也可以在其他Configuration中注入)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 数据源
@Autowired
DataSource dataSource;
// UserDetailsService实现类
@Autowired
UserDetailsService userDetailsService;
// 注入密码加密解密类
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
// 注入jdbc Token操作类
@Bean
PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
repository.setCreateTableOnStartup(false); //关闭自动创建表
repository.setDataSource(dataSource); //注入数据源
return repository;
}
}
- 配置用户登录密码校验,这里就是查询数据库校验账号密码的有效性
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 密码需要进行加密解密进行验证匹配!
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
- 配置登录、登出、记住我、Cookie有效时间
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login") // 登录页面
.loginProcessingUrl("/user/login") // 提交表单处理的请求,由Security实现
.defaultSuccessUrl("/index",true).permitAll() //成功访问哪里
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/index").deleteCookies().permitAll() //退出成功页面
// 2. 无需保护的页面
.and()
.authorizeRequests()
.antMatchers("/level1/**").permitAll()
.antMatchers("/level2/**").hasAnyRole("customer", "admin")
.antMatchers("/level3/**").hasRole("admin")
.anyRequest().authenticated().and()
.rememberMe() //记住我
.tokenRepository(persistentTokenRepository) //注入操作token的jdbc
.tokenValiditySeconds(60).rememberMeCookieName("remember-me") //Cookie有效时间 单位: 秒
.userDetailsService(userDetailsService); //注入用户验证UserDetailsService
http.exceptionHandling().accessDeniedPage("/nodeny");
http.csrf().disable();
}
2.4、编写前端登录页面
这里一定要开启checkbox复选框,并且这个name = “remember-me”。
<form action="/user/login" method="post">
用户名: <input type="text" name="username"><br/>
密码: <input type="text" name="password"><br/>
<input type="checkbox" name="remember-me">记住我<br/>
<input type="submit" value="登录"/>
</form>