认证
# 登录校验流程
# 过滤器链
SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器:
UsernamePasswordAuthenticationFilter:负责处理在登陆页面填写了用户名密码后的登陆请求;
ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException ;
FilterSecurityInterceptor:负责权限校验的过滤器;
# 认证流程
Authentication接口: 封装用户相关信息;
**AuthenticationManager接口:**其实现类调用DaoAuthenticationProvider的authenticate方法进行认证;
**UserDetailsService接口:**加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法;
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中;
# 代码实现
# 思路
登录:
- 自定义登录接口:调用ProviderManager的authenticate方法进行认证,如果认证通过生成jwt把用户信息存入redis中;
- 自定义UserDetailService:其实现类去数据库查询用户信息;
校验:
定义JWT认证过滤器:
- 获取token;
- 解析token获取其中userId;
- 从redis获取用户信息;
- 存入SecurityContextHolder;
# 添加依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--SpringSecurity启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.15</version>
</dependency>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--common-pool-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# 数据库校验用户
实现UserDetailsService接口,从数据库查询用户:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(queryWrapper);
if(Objects.isNull(user)){
throw new RuntimeException("用户名密码错误");
}
return new LoginUser(user);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LoginUser包装UserDetails(后续还能封装权限):
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 密码加密存储
配置类SecurityConfig中加入Bean:
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
2
3
4
测试:
@Test
public void TestBCryptPasswordEncoder(){
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
System.out.println(bCryptPasswordEncoder.encode("123"));
System.out.println(bCryptPasswordEncoder.matches("123","$2a$10$C0V/bLHc7cmtuE11kWtkputF56PNr3miVAz5JsTT/exjLkPZkF9Ei"));
}
2
3
4
5
6
将数据库中密码修改为加密后的;
# 登录接口
自定义登录接口,让SpringSecurity对这个接口放行,用户不登陆也能访问;
接口中通过AuthenticationManager的authenticate方法进行用户验证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()
//其它接口需要鉴权认证
.anyRequest().authenticated();
//把token校验过滤器添加到过滤器链中
http
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
认证成功后生成jwt,为了让用户下回请求时能通过jwt识别出具体的是哪个用户,可以把用户信息存入redis,可以把用户id作为key;
LoginServiceImpl
@Override
public ResponseResult login(User user) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if(Objects.isNull(authenticate)) {
throw new RuntimeException("用户名或密码错误");
}
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
HashMap<String,Object> tokenMap = new HashMap<>();
tokenMap.put("userId",userId);
String jwt = JwtUtil.createToken(tokenMap,HOUR,24);
redisTemplate.opsForValue().set("login:"+userId, JSON.toJSONString(loginUser));
HashMap<String, String> map = new HashMap<>();
map.put("token",jwt);
return new ResponseResult(200,"登录成功",map);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
到这仅将jwt返回给前端,前端需要根据请求头中的token,进行解析出userId,去redis获取对应LoginUser对象,封装为Authentication对象存入SecurityContextHolder;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token");
if(!StringUtils.hasText(token)) {
filterChain.doFilter(request, response);
return ;
}
//解析token
String userId;
try {
userId = (String) JwtUtil.parseToken(token, "userId");
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis中获取用户信息
String redisKey = "login:" + userId;
String jsonStr = (String) redisTemplate.opsForValue().get(redisKey);
LoginUser loginUser = JSON.parseObject(jsonStr,LoginUser.class);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
//存入SecurityContextHolder
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
此过滤器链需要在其它过滤器之前,在SecurityConfig中的addFilterBefore中配置;
# 退出登录
获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可;
@Override
public ResponseResult logout() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long userId = loginUser.getUser().getId();
redisTemplate.delete("login:"+userId);
return new ResponseResult(200,"退出成功");
}
2
3
4
5
6
7
8
# 测试
登录测试:
登出测试: