SpringSecurity微服务权限方案

image-20221109210902180

1.认证授权过程分析

微服务本质:

image-20221109210128213

image-20221109210445793

需求分析

image-20221109211556120

数据模型介绍:

image-20221109214015143

使用技术说明:

image-20221110144805581

搭建项目工程:

对应教程视频:25-尚硅谷-SpringSecurity-微服务权限案例-引入项目依赖_哔哩哔哩_bilibili

image-20221110145058838

image-20221110150906119

启动Redis和Nacos:

image-20221110180707351

编写common工具类:

对应教程视频:27-尚硅谷-SpringSecurity-微服务权限案例-编写common工具类_哔哩哔哩_bilibili

image-20221110184003848

编写security工具类:

对应教程视频:28-尚硅谷-SpringSecurity-微服务权限案例-编写security工具类_哔哩哔哩_bilibili

image-20221110205032587

2.1 密码处理工具类

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
import atguigu.utils.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
* @author : 其然乐衣Letitbe
* @date : 2022/11/10
*/
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {

public DefaultPasswordEncoder() {
// 调用有参构造方法,传值 -1
this(-1);
}

public DefaultPasswordEncoder(int strength) {

}

/**
* 进行MD5加密
*/
@Override
public String encode(CharSequence charSequence) {
return MD5.encrypt(charSequence.toString());
}

/**
* 进行密码比对
* 比对一样的话返回 true,不一样的话返回false
* @param charSequence 加密后的密码
* @param encodedPassword 传入的密码
* @return
*/
@Override
public boolean matches(CharSequence charSequence, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(charSequence.toString()));
}
}

2.2 token操作工具类

先导入依赖:

1
2
3
4
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>

使用jwt生成token代码:

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
package com.atguigu.security.security;

import io.jsonwebtoken.CompressionCodecs;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
* token工具类
* @author : 其然乐衣Letitbe
* @date : 2022/11/10
*/
@Component
public class TokenManager {

/**
* token有效时长
*/
private long tokenExpiration = 24 * 60 * 60 * 1000;
/**
* 编码密钥
*/
private String tokenSignKey = "123456";

/**
* 1. 使用 jwt 根据用户名生成token
*/
public String createToken(String username) {
String token = Jwts.builder()
// 设置主体信息
.setSubject(username)
// 设置有效时长
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
return token;
}

/**
* 2. 根据token字符串得到用户信息
*/
public String getUserInfoFromToken(String token) {
String userinfo = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
return userinfo;
}

/**
* 3. 删除token
* 但是这个删除方法其实不需要我们写,因为token不需要咱们删,客户端不携带 token 就可以了
*/
public void removeToken(String token) {}
}

2.3 退出处理器

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
import atguigu.utils.utils.R;
import atguigu.utils.utils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* 退出处理器
* @author : 其然乐衣Letitbe
* @date : 2022/11/10
*/
public class TokenLogoutHandler implements LogoutHandler {

private TokenManager tokenManager;
private RedisTemplate redisTemplate;

public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}

@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
// 1. 从header里面获取token
// 2. token不为空,移除token, 从redis删除token
String token = request.getHeader("token");
if (token != null) {
// 移除
tokenManager.removeToken(token);

// 从token获取用户名
String username = tokenManager.getUserInfoFromToken(token);
// key : username
redisTemplate.delete(username);
}
ResponseUtil.out(response, R.ok());
}
}

2.4 未授权统一处理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import atguigu.utils.utils.R;
import atguigu.utils.utils.ResponseUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

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

/**
* 未授权统一处理类
* @author : 其然乐衣Letitbe
* @date : 2022/11/10
*/
public class UnauthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}

编写security认证过滤器:

对应教程视频;29-尚硅谷-SpringSecurity-微服务权限案例-编写security认证过滤器_哔哩哔哩_bilibili

1. 认证的过滤器

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
77
78
79
80
81
82
83
84
85
86
import atguigu.utils.utils.R;
import atguigu.utils.utils.ResponseUtil;
import com.atguigu.security.entity.SecurityUser;
import com.atguigu.security.entity.User;
import com.atguigu.security.security.TokenManager;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

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.ArrayList;

/**
* @author : 其然乐衣Letitbe
* @date : 2022/11/10
*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

private TokenManager tokenManager;
private RedisTemplate redisTemplate;
/**
* 由springsecurity封装的
*/
private AuthenticationManager authenticationManager;

public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
// 设置为不仅仅是post提交
this.setPostOnly(false);
// 设置登录路径,且匹配post提交
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login", "POST"));
}

/**
* 1. 获取表单提交用户名和密码
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 获取表单提交数据
try {
User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(),
new ArrayList<>()));
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException();
}
}

/**
* 2. 认证成功时会调用的方法
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
// 这个方法可以获取认证通过后的用户的信息,然后根据用户名生成token,再把它放到redis中
SecurityUser user = (SecurityUser) authResult.getPrincipal();
// 根据用户名生成token
String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
// 把用户名称和用户权限列表放到redis
redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());
// 返回token
ResponseUtil.out(response, R.ok().data("token", token));
}

/**
* 3. 认证失败时会调用的方法
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
// 返回错误的提示
ResponseUtil.out(response, R.error());
}
}

2.授权过滤器

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
import com.atguigu.security.security.TokenManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

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.ArrayList;
import java.util.Collection;
import java.util.List;

/**
* @author : 其然乐衣Letitbe
* @date : 2022/11/10
*/
public class TokenAuthFilter extends BasicAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;

public TokenAuthFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 获取当前认证成功用户权限信息
UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
// 判断如果有权限信息,放到权限上下文中
if (authRequest != null) {
SecurityContextHolder.getContext().setAuthentication(authRequest);
}
chain.doFilter(request, response);
}

private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
// 从header获取token
String token = request.getHeader("token");
if (token != null) {
// 从token获取用户
String username = tokenManager.getUserInfoFromToken(token);

//从redis获取对应权限列表
List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(username);
Collection<GrantedAuthority> authorities = new ArrayList<>();
for ( String permissionValue : permissionValueList ) {
SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue);
authorities.add(auth);
}
return new UsernamePasswordAuthenticationToken(username, token, authorities)
}
return null;
}
}

编写entity:

SecurityUser实体类:

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
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
* @author : 其然乐衣Letitbe
* @date : 2022/11/10
*/
@Data
@Slf4j
public class SecurityUser implements UserDetails {
//当前登录用户
private transient User currentUserInfo;
//当前权限
private List<String> permissionValueList;
public SecurityUser() {
}
public SecurityUser(User user) {
if (user != null) {
this.currentUserInfo = user;
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) {
continue;
}
SimpleGrantedAuthority authority = new
SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return currentUserInfo.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;
}
}

User用户实体类:

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
package com.atguigu.security.entity;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;

/**
* @author : 其然乐衣Letitbe
* @date : 2022/11/10
*/
@Data
@ApiModel(description = "用户实体类")
public class User implements Serializable {

private static final long serialVersionUID = 1L;

/**
* @ApiModelProperty用于swapper测试的
*/
@ApiModelProperty(value = "微信openid")
private String username;

@ApiModelProperty(value = "密码")
private String password;

@ApiModelProperty(value = "昵称")
private String nickName;

@ApiModelProperty(value = "用户头像")
private String salt;

@ApiModelProperty(value = "用户签名")
private String token;
}

整合网关和前端:

解决跨域配置:

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
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.cors.reactive.CorsConfigurationSource;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.util.pattern.PathPatternParser;

/**
* @author : 其然乐衣Letitbe
* @date : 2022/11/11
*/
@Configuration
public class CorsConfig {

/**
* 解决跨域
*/
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration config = new CorsConfiguration();
// 表示该服务器允许任何类型的请求
config.addAllowedMethod("*");
// 允许任何域名来源的请求
config.addAllowedOrigin("*");
// 允许携带任何请求头
config.addAllowedHeader("*");

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
// 表示所有路径都允许访问
source.registerCorsConfiguration("/**", config);

return new CorsWebFilter((CorsConfigurationSource) source);
}
}

配置文件:

application.properties:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 端口号
server.port=8222
# 服务名
spring.application.name=service-gateway
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=222.177.66.230:8848
# 使服务发现路由
spring.cloud.gateway.discovery.locator.enabled=true

# 配置路由规则
spring.cloud.gateway.routes[0].id=service-acl
# 设置路由uri lb://注册服务名称
spring.cloud.gateway.routes[0].uri=lb://service-acl
# 具体路径规则
spring.cloud.gateway.routes[0].predicates= path=/*/acl/**

整合网关:

image-20221111165948445