spring-security

Spring Security

简介

它是spring家族中的一个安全框架,核心功能是授权与认证

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。提供了完善的认证机制和方法级的授权功能。是一款非常优秀的权限管理框架。它的核心是一组过滤器链,不同的功能经由不同的过滤器

一般的web应用需要进行授权和认证。

  • 认证:验证当前访问系统的是不是本系统用户,并且具体到那一个用户
  • 授权:经过认证后判定当前用户是否有权限执行某个操作

1.简单上手

1.1 准备工作

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

1.2数据库验证

1.2.1引入依赖

1
2
3
4
5
6
7
8
9
<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>

配置数据库源:

1
2
3
4
5
6
spring:
datasource:
url: jdbc:mysql://localhost:3306/sg_security?characterEncoding=utf-8&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver

实体类:

1
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
72
73
74
75
76
import java.io.Serializable;
import java.util.Date;


/**
* 用户表(User)实体类
*
* @author 三更
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = -40356785423868312L;

/**
* 主键
*/
private Long id;
/**
* 用户名
*/
private String userName;
/**
* 昵称
*/
private String nickName;
/**
* 密码
*/
private String password;
/**
* 账号状态(0正常 1停用)
*/
private String status;
/**
* 邮箱
*/
private String email;
/**
* 手机号
*/
private String phonenumber;
/**
* 用户性别(0男,1女,2未知)
*/
private String sex;
/**
* 头像
*/
private String avatar;
/**
* 用户类型(0管理员,1普通用户)
*/
private String userType;
/**
* 创建人的用户id
*/
private Long createBy;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新人
*/
private Long updateBy;
/**
* 更新时间
*/
private Date updateTime;
/**
* 删除标志(0代表未删除,1代表已删除)
*/
private Integer delFlag;
}

1.2.2 实现UserDetailsService

1
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
package com.xu.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xu.dao.UserMapper;
import com.xu.pojo.LoginUser;
import com.xu.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Objects;

/**
* @author ==> 许帅帅
* @version ==> 1.0
* 2023/5/6 10:04
*/
@Service
public class UserDetailServiceImpl implements UserDetailsService {

@Autowired
private UserMapper mapper;


@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,s);

User user = mapper.selectOne(queryWrapper);

if (Objects.isNull(user)){
throw new RuntimeException("用户不存在!");
}

//将用户信息封装返回
return new LoginUser(user);
}
}

1.2.3 实现UserDetails

实现这个接口,注入我们自己的实体类,生成构造器和有参数无参数getter和setter方法

1
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
package com.xu.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
* @author ==> 许帅帅
* @version ==> 1.0
* 2023/5/6 10:11
*/
@Data
@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;
}
}

1.2.4 选择加密方式

继承WebSecurityConfigurerAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.xu.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* @author ==> 许帅帅
* @version ==> 1.0
* 2023/5/6 10:21
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}

2.认证

2.1登录校验流程

image-20230504190709799

2.2 spring security执行流程

本质上是一个过滤器链,包含了各种功能的过滤器

image-20230506204623402

UsernamePasswordAuthenticationFilter:对login的post请求做拦截,校验表单中的用户名密码

FilterSecurityInterceptor :是一个方法=级的权限过滤器,基本位于过滤器最底部

ExceptionTranslationFilter: 是一个异常处理器,用来处理在权限认证过程中的异常处理

2.3 认证流程

image-20230506204650947

概念速查
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。AuthenticationManager接口: 定义了认证Authentication的方法UserDetailsService接口: 加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法UserDetails接口: 提供核心用户信息,通过UserDetailservice根据用户名获取外理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

2.4 登陆验证

image-20230504194318161

image-20230504194335762

解决思路

登录

  • 自定义登录接口,调用ProviderManager的方法进行认证,

    如果认证通过,生成jwt

    把用户信息存入redis

  • 自定义UserDetailsService

    在这个实现类去查询数据库

校验

  • 定义jwt过滤器

    获取token

    解析token,获取userid

    从redis获取用户信息

    存入SecurityContextHoder

2.4.1登录接口

​ 自定义登陆接口,需要让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。

​ 在接口中我们通过AuthenticationManagerauthenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器

​ 认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。

登录接口

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class LoginController {

@Autowired
private LoginServcie loginServcie;

@PostMapping("/user/login")
public ResponseResult login(@RequestBody User user){
return loginServcie.login(user);
}
}

配置类,配置security规则,

1
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
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
}

//将AuthenticationManager注入到IOC容器,用来获取Authentication对象
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
1
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
package com.xu.service.impl;

import com.xu.pojo.LoginUser;
import com.xu.pojo.User;
import com.xu.service.LoginService;
import com.xu.util.JwtUtil;
import com.xu.util.RedisCache;
import com.xu.util.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Objects;

/**
* @author ==> 许帅帅
* @version ==> 1.0
* 2023/5/6 10:57
*/
@Service
public class LoginServiceImpl implements LoginService {

@Autowired
private RedisCache redisCache;

@Autowired
private AuthenticationManager authenticationManager;

@Override
public ResponseResult login(User user) {
//调用AuthenticationManager的认证方法
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);

if (Objects.isNull(authenticate)){
throw new RuntimeException("认证失败!");
}

//认证通过使用useId生成一个jwt
LoginUser principal = (LoginUser) authenticate.getPrincipal();
Long id = principal.getUser().getId();
String jwt = JwtUtil.createJWT(id.toString());

HashMap<String, String> map = new HashMap<>();
map.put("token",jwt);

//将完整用户信息存入redis
redisCache.setCacheObject("login:"+id,principal);

return new ResponseResult(200,"登陆成功",map);
}
}

2.4.2 认证过滤器

​ 我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。

​ 使用userid去redis中获取对应的LoginUser对象。

​ 然后封装Authentication对象存入SecurityContextHolder

1
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
72
73
package com.xu.filter;

import com.xu.pojo.LoginUser;
import com.xu.util.JwtUtil;
import com.xu.util.RedisCache;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;

/**
* @author ==> 许帅帅
* @version ==> 1.0
* 2023/5/6 13:03
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

@Autowired
private RedisCache redisCache;

@Override
protected void
doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain)
throws ServletException, IOException {
String token = httpServletRequest.getHeader("token");

if (!StringUtils.hasText(token)) {
//放行
filterChain.doFilter(httpServletRequest, httpServletResponse);
throw new RuntimeException("token是空的!");
}

String userID;
//解析token
try {
Claims claims = JwtUtil.parseJWT(token);
userID = claims.getSubject();
} catch (Exception e) {
throw new RuntimeException("token解析异常!");
}

//从redis里面获取信息
String key = "login:" + userID;
LoginUser loginUser = redisCache.getCacheObject(key);

if(Objects.isNull(loginUser)){
throw new RuntimeException("redis 信息出现错误!");
}

//创建Authentication对象
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(loginUser, null, null);

//存入SecurityHolder
SecurityContextHolder.getContext().setAuthentication(authenticationToken);

//放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}

将过滤器添加到Spring Security过滤器链中

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private JwtAuthenticationTokenFilter filter;

@Override
protected void configure(HttpSecurity http) throws Exception {
//将自定义的过滤器链添加到Security中去
http.addFilterBefore(filter,UsernamePasswordAuthenticationFilter.class);
}
}

2.4 退出登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public ResponseResult logout() {
//获取SecurityContextHolder里面的用户id
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();

String ID = loginUser.getUser().getId().toString();

String redisKey = "login:" + ID;

//删除redis中对应的用户数据
redisCache.deleteObject(redisKey);

return new ResponseResult(200,"注销成功!");
}

image-20230506212012433