Spring Security 5 基础教程之四微服务权限实战

大纲

CSRF

CSRF 介绍

跨站请求伪造 (Cross-site request forgery),也被称为 one-clickattack 或者 session riding,通常缩写为 CSRF 或者 XSRF,是一种挟持用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本 (XSS) 相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作 (如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。这利用了 Web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。

CSRF 使用

在 Form 表单中添加一个隐藏标签,如下所示:

1
<input type="hidden" th:if="${_csrf}!=null" th:value="${_csrf.token}" name="_csrf"/>

Spring Security 默认启用 CSRF,若手动禁用了 CSRF,则需要在配置类中注释掉相关代码,如下所示:

1
// http.csrf().disable(); 

CSRF 实现原理

  • 生成 CsrfToken,并保存到 HttpSession 或者 Cookie 中

  • SaveOnAccessCsrfToken 类有个接口 CsrfTokenRepository

  • CookieCsrfTokenRepository 实现类的底层源码
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
public final class CookieCsrfTokenRepository implements CsrfTokenRepository {

static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;
private String headerName = DEFAULT_CSRF_HEADER_NAME;
private String cookieName = DEFAULT_CSRF_COOKIE_NAME;
private boolean cookieHttpOnly = true;
private String cookiePath;
private String cookieDomain;

public CookieCsrfTokenRepository() {

}

......

@Override
public CsrfToken generateToken(HttpServletRequest request) {
return new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken());
}

private String createNewToken() {
return UUID.randomUUID().toString();
}

}
  • 当请求到来时,Spring Security 从请求中获取 CsrfToken,并与已保存的 CsrfToken 做比较,进而判断当前请求是否合法,整个过程主要是通过 CsrfFilter 过滤器来完成

微服务权限实战

这里主要演示如何在微服务系统中,基于 Spring Cloud、Spring Security、JWT、RBAC 权限模型实现权限管理。

版本说明

本案例使用了以下技术,具体版本说明如下:

技术版本说明
Spring Boot2.2.1.RELEASE
Spring CloudHoxton.RELEASE 引入了 Gateway 组件,用于 API 网关
Spring Cloud Alibaba0.2.2.RELEASE 引入了 Nacos 组件,用于注册中心
Spring Security5.2.1.RELEASE
MyBatis-Plus3.0.5
Swagger2.7.0
Redis6.0.4
JWT0.7.0

实现思路

  • 如果是基于 Session,那么 Spring Security 会对 Cookie 里的 sessionid 进行解析,找到服务器端存储的 Session 信息,然后判断当前用户是否符合请求的要求。
  • 如果是基于 Token,则是解析出 Token,然后将当前请求加入到 Spring Security 管理的权限信息中去,完整的认证与授权流程和图解如下:
    • (1) 由于系统的模块众多,每个模块都需要进行授权与认证,所以一般选择基于 Token 的形式进行认证与授权。
    • (2) 用户根据用户名与密码认证成功后,获取当前用户角色的一系列权限值,以用户名为 Key,权限列表为 Value 的形式存入 Redis 缓存;同时根据用户名相关信息生成 Token 并返回浏览器,然后浏览器将 Token 存入 Cookie。
    • (3) 前端每次调用 API 接口的时候,默认将 Cookie 中的 Token 携带到 Header 请求头。
    • (4) Spring Security 处理请求的 Header 头,从中解析 Token 信息,得到当前用户名,根据用户名就可以从 Redis 中获取权限列表,这样 Spring Security 就能够判断当前请求是否有权限访问。

权限模型

权限模型有两种,分别是 ACL 和 RBAC。

ACL 模型

  • ACL (Access Controll List)
    • 用户(t_user)
    • 用户_权限 (t_user_perm)
      • N 对 N 关系,需要有中间表
    • 权限(t_permission)

RBAC 模型

  • RBAC (Role Based Access Controll)
    • 用户 (t_user)
    • 用户_角色 (t_user_role)
      • N 对 N 关系,需要有中间表
    • 角色 (t_role)
    • 角色_权限 (t_role_perm)
      • N 对 N 关系,需要有中间表
    • 权限 (t_permission)

RBAC 权限模型对应的数据库表设计如下:

JWT 概述

实战案例

项目结构

初始化数据库

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
87
88
89
90
91
-- 创建数据库
CREATE DATABASE `micro_service_acl` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 切换数据库
USE `micro_service_acl`;

-- 创建用户表
CREATE TABLE `acl_user` (
`id` char(19) NOT NULL COMMENT '会员id',
`username` varchar(20) NOT NULL DEFAULT '' COMMENT '微信openid',
`password` varchar(32) NOT NULL DEFAULT '' COMMENT '密码',
`nick_name` varchar(50) DEFAULT NULL COMMENT '昵称',
`salt` varchar(255) DEFAULT NULL COMMENT '用户头像',
`token` varchar(100) DEFAULT NULL COMMENT '用户签名',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 创建角色表
CREATE TABLE `acl_role` (
`id` char(19) NOT NULL DEFAULT '' COMMENT '角色id',
`role_name` varchar(20) NOT NULL DEFAULT '' COMMENT '角色名称',
`role_code` varchar(20) DEFAULT NULL COMMENT '角色编码',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 创建权限表
CREATE TABLE `acl_permission` (
`id` char(19) NOT NULL DEFAULT '' COMMENT '编号',
`pid` char(19) NOT NULL DEFAULT '' COMMENT '所属上级',
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '名称',
`type` tinyint(3) NOT NULL DEFAULT '0' COMMENT '类型(1:菜单,2:按钮)',
`permission_value` varchar(50) DEFAULT NULL COMMENT '权限值',
`path` varchar(100) DEFAULT NULL COMMENT '访问路径',
`component` varchar(100) DEFAULT NULL COMMENT '组件路径',
`icon` varchar(50) DEFAULT NULL COMMENT '图标',
`status` tinyint(4) DEFAULT NULL COMMENT '状态(0:禁止,1:正常)',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
`gmt_create` datetime DEFAULT NULL COMMENT '创建时间',
`gmt_modified` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_pid` (`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限';

-- 创建用户角色表
CREATE TABLE `acl_user_role` (
`id` char(19) NOT NULL DEFAULT '' COMMENT '主键id',
`role_id` char(19) NOT NULL DEFAULT '0' COMMENT '角色id',
`user_id` char(19) NOT NULL DEFAULT '0' COMMENT '用户id',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_role_id` (`role_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 创建角色权限表
CREATE TABLE `acl_role_permission` (
`id` char(19) NOT NULL DEFAULT '',
`role_id` char(19) NOT NULL DEFAULT '',
`permission_id` char(19) NOT NULL DEFAULT '',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_role_id` (`role_id`),
KEY `idx_permission_id` (`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色权限';

-- 插入用户数据
INSERT INTO `acl_user` VALUES ('1','admin','96e79218965eb72c92a549dd5a330112','admin','',NULL,0,'2018-05-01 10:39:47','2018-05-01 10:39:47'),('2','test','96e79218965eb72c92a549dd5a330112','test',NULL,NULL,0,'2018-05-01 16:36:07','2018-05-01 16:40:08');

-- 插入角色数据
INSERT INTO `acl_role` VALUES ('1','普通管理员',NULL,NULL,0,'2018-05-11 13:09:32','2018-05-18 10:27:18'),('2','测试员',NULL,NULL,0,'2018-05-18 13:35:58','2018-05-18 13:35:58');

-- 插入权限数据
INSERT INTO `acl_permission` VALUES ('1','0','全部数据',0,NULL,NULL,NULL,NULL,NULL,0,'2018-05-15 17:13:06','2018-05-15 17:13:06'),('1195268474480156673','1','权限管理',1,NULL,'/acl','Layout',NULL,NULL,0,'2018-05-15 17:13:06','2018-05-18 13:54:25'),('1195268616021139457','1195268474480156673','用户管理',1,NULL,'user/list','/acl/user/list',NULL,NULL,0,'2018-05-15 17:13:40','2018-05-18 13:53:12'),('1195268788138598401','1195268474480156673','角色管理',1,NULL,'role/list','/acl/role/list',NULL,NULL,0,'2018-05-15 17:14:21','2018-05-15 17:14:21'),('1195268893830864898','1195268474480156673','菜单管理',1,NULL,'menu/list','/acl/menu/list',NULL,NULL,0,'2018-05-15 17:14:46','2018-05-15 17:14:46'),('1195269143060602882','1195268616021139457','查看',2,'user.list','','',NULL,NULL,0,'2018-05-15 17:15:45','2018-05-17 21:57:16'),('1195269295926206466','1195268616021139457','添加',2,'user.add','user/add','/acl/user/form',NULL,NULL,0,'2018-05-15 17:16:22','2018-05-15 17:16:22'),('1195269473479483394','1195268616021139457','修改',2,'user.update','user/update/:id','/acl/user/form',NULL,NULL,0,'2018-05-15 17:17:04','2018-05-15 17:17:04'),('1195269547269873666','1195268616021139457','删除',2,'user.remove','','',NULL,NULL,0,'2018-05-15 17:17:22','2018-05-15 17:17:22'),('1195269821262782465','1195268788138598401','修改',2,'role.update','role/update/:id','/acl/role/form',NULL,NULL,0,'2018-05-15 17:18:27','2018-05-15 17:19:53'),('1195269903542444034','1195268788138598401','查看',2,'role.list','','',NULL,NULL,0,'2018-05-15 17:18:47','2018-05-15 17:18:47'),('1195270037005197313','1195268788138598401','添加',2,'role.add','role/add','/acl/role/form',NULL,NULL,0,'2018-05-15 17:19:19','2018-05-18 11:05:42'),('1195270442602782721','1195268788138598401','删除',2,'role.remove','','',NULL,NULL,0,'2018-05-15 17:20:55','2018-05-15 17:20:55'),('1195270621548568578','1195268788138598401','角色权限',2,'role.acl','role/distribution/:id','/acl/role/roleForm',NULL,NULL,0,'2018-05-15 17:21:38','2018-05-15 17:21:38'),('1195270744097742849','1195268893830864898','查看',2,'permission.list','','',NULL,NULL,0,'2018-05-15 17:22:07','2018-05-15 17:22:07'),('1195270810560684034','1195268893830864898','添加',2,'permission.add','','',NULL,NULL,0,'2018-05-15 17:22:23','2018-05-15 17:22:23'),('1195270862100291586','1195268893830864898','修改',2,'permission.update','','',NULL,NULL,0,'2018-05-15 17:22:35','2018-05-15 17:22:35'),('1195270887933009922','1195268893830864898','删除',2,'permission.remove','','',NULL,NULL,0,'2018-05-15 17:22:41','2018-05-15 17:22:41'),('1196301740985311234','1195268616021139457','分配角色',2,'user.assgin','user/role/:id','/acl/user/roleForm',NULL,NULL,0,'2018-05-18 13:38:56','2018-05-18 13:38:56');

-- 插入角色权限数据
INSERT INTO `acl_role_permission` VALUES ('1196301979754455041','1','1',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301979792203778','1','1195268474480156673',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301979821563906','1','1195268616021139457',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301979842535426','1','1195269143060602882',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301979855118338','1','1195269295926206466',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301979880284161','1','1195269473479483394',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301979913838593','1','1195269547269873666',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301979926421506','1','1196301740985311234',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301979951587330','1','1195268788138598401',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301980014501889','1','1195269821262782465',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301980035473410','1','1195269903542444034',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301980052250626','1','1195270037005197313',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301980077416450','1','1195270442602782721',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301980094193665','1','1195270621548568578',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301980119359489','1','1195268893830864898',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301980136136706','1','1195270744097742849',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301980249382913','1','1195270810560684034',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301980270354434','1','1195270862100291586',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301980287131649','1','1195270887933009922',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301980287131748','2','1',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301980287131835','2','1195268474480156673',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301980287131836','2','1195268616021139457',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301980287131837','2','1195269143060602882',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301980287131838','2','1195269295926206466',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301980287131839','2','1195269473479483394',0,'2018-05-18 13:39:53','2018-05-18 13:39:53'),('1196301980287131840','2','1195269547269873666',0,'2018-05-18 13:39:53','2018-05-18 13:39:53');

-- 插入用户角色数据
INSERT INTO `acl_user_role` VALUES ('1','1','1',0,'2018-05-11 13:09:53','2018-05-11 13:09:53'),('2','2','2',0,'2018-05-11 13:09:53','2018-05-11 13:09:53');

核心业务代码

common-security 模块

common-security 模块的代码结构

点击 查看 common-security 模块的代码结构。

  • 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
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;

@Data
@ApiModel(description = "用户实体类")
public class User implements Serializable {

@ApiModelProperty(value = "用户名")
private String username;

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

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

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

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

}
  • 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
68
69
70
71
72
73
74
75
76
77
78
import com.clay.common.security.entity.User;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;

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

@Data
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;
}

}
  • TokenLoginFilter
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
import com.clay.common.base.utils.R;
import com.clay.common.base.utils.ResponseUtil;
import com.clay.common.security.entity.User;
import com.clay.common.security.helper.TokenManager;
import com.clay.common.security.model.SecurityUser;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
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;

/**
* 认证过滤器
*/
@Slf4j
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

private TokenManager tokenManager;

private RedisTemplate redisTemplate;

private AuthenticationManager authenticationManager;

public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.authenticationManager = authenticationManager;
this.setPostOnly(false);
// 登录接口的路径
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login", "POST"));
}

/**
* 执行认证
*/
@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()));
} catch (IOException e) {
log.error(e.getMessage());
throw new RuntimeException();
}
}

/**
* 认证成功的处理
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
// 认证成功后的用户信息
SecurityUser user = (SecurityUser) authResult.getPrincipal();

// 根据用户名生成Token
String token = tokenManager.createToken(user.getUsername());

// 将用户权限列表存入缓存
redisTemplate.opsForValue().set(user.getUsername(), user.getPermissionValueList());

// 返回Token
ResponseUtil.out(response, R.ok().data("token", token));
}

/**
* 认证失败的处理
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
ResponseUtil.out(response, R.error().message("认证失败"));
}

}
  • TokenAuthenticationFilter
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
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.clay.common.security.helper.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 org.springframework.util.StringUtils;

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;

/**
* 授权过滤器
*/
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {

private TokenManager tokenManager;

private RedisTemplate redisTemplate;

public TokenAuthenticationFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
super(authenticationManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 当前认证成功用户的权限信息
UsernamePasswordAuthenticationToken authResult = getAuthentication(request);

// 将用户的权限信息放到权限上下文中
if (authResult != null) {
SecurityContextHolder.getContext().setAuthentication(authResult);
}

chain.doFilter(request, response);
}

/**
* 获取认证成功用户的权限信息
*/
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
// 从Header获取Token
String token = request.getHeader("token");
if (StrUtil.isNotBlank(token)) {
// 根据Token获取用户信息
String username = tokenManager.getUserInfoFromToken(token);

// 从缓存中获取用户的权限列表
List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(username);

// 封装用户的权限列表
Collection<GrantedAuthority> authorities = new ArrayList<>();
if (CollectionUtil.isNotEmpty(permissionValueList)) {
for (String permissionValue : permissionValueList) {
if (StringUtils.isEmpty(permissionValue)) {
continue;
}
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
}
return new UsernamePasswordAuthenticationToken(username, token, authorities);
}
return null;
}

}
  • TokenLogoutHandler
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
package com.clay.common.security.helper;

import cn.hutool.core.util.StrUtil;
import com.clay.common.base.utils.R;
import com.clay.common.base.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;

/**
* 退出登录处理器
*/
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) {
String token = request.getHeader("token");
if (StrUtil.isNotBlank(token)) {
String username = tokenManager.getUserInfoFromToken(token);
// 删除缓存数据
if (StrUtil.isNotBlank(username)) {
redisTemplate.delete(username);
}
}
ResponseUtil.out(response, R.ok());
}

}
  • UnauthorizedEncryPoint
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.clay.common.base.utils.R;
import com.clay.common.base.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;

/**
* 未授权处理器
*/
public class UnauthorizedEncryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ResponseUtil.out(response, R.error().message("未授权"));
}

}
  • DefaultPasswordEncoder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.clay.common.security.helper;

import com.clay.common.base.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
* 密码处理器
*/
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {

@Override
public String encode(CharSequence rawPassword) {
// MD5 加密
return MD5.encrypt(rawPassword.toString());
}

@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(this.encode(rawPassword));
}

}
  • TokenManager
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
package com.clay.common.security.helper;

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

import java.util.Date;

/**
* Token操作工具
*/
@Component
public class TokenManager {

/**
* 签名秘钥
*/
private String tokenSignKey = "123456";

/**
* Token的有效时长
*/
private long tokenExpireSeconds = 24 * 60 * 60 * 1000;

/**
* 生成Token
*
* @param username
* @return
*/
public String createToken(String username) {
Date expireDate = new Date(System.currentTimeMillis() + tokenExpireSeconds);
return Jwts.builder()
.setSubject(username)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
}

/**
* 根据Token获取用户信息
*
* @param token
* @return
*/
public String getUserInfoFromToken(String token) {
return Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
}

}
  • TokenWebSecurityConfig
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
import com.clay.common.security.filter.TokenAuthenticationFilter;
import com.clay.common.security.filter.TokenLoginFilter;
import com.clay.common.security.helper.DefaultPasswordEncoder;
import com.clay.common.security.helper.TokenLogoutHandler;
import com.clay.common.security.helper.TokenManager;
import com.clay.common.security.helper.UnauthorizedEncryPoint;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;

/**
* 安全配置
*/
@Configuration
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {

private TokenManager tokenManager;
private RedisTemplate redisTemplate;
private UserDetailsService userDetailsService;
private DefaultPasswordEncoder passwordEncoder;

public TokenWebSecurityConfig(TokenManager tokenManager, RedisTemplate redisTemplate, UserDetailsService userDetailsService, DefaultPasswordEncoder passwordEncoder) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new UnauthorizedEncryPoint()) // 配置未授权处理器
.and().csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and().logout().logoutUrl("/admin/acl/index/logout") // 配置退出登录的 URL
.addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)).and() // 配置退出登录处理器
.addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate)) // 配置认证过滤器
.addFilter(new TokenAuthenticationFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic(); // 配置授权过滤器
}

@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// 配置UserDetailsService和密码解析器
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}

@Override
public void configure(WebSecurity web) throws Exception {
// 配置哪些路径可以不进行认证,可以直接访问
web.ignoring().antMatchers("/api/**");
}

}
service-acl 模块

service-acl 模块的代码结构

点击 查看 service-acl 模块的代码结构。

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
import com.clay.acl.entity.User;
import com.clay.acl.service.PermissionService;
import com.clay.acl.service.UserService;
import com.clay.common.security.model.SecurityUser;
import org.springframework.beans.BeanUtils;
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.List;

/**
* Spring Security 用户信息服务
*/
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {

@Autowired
private UserService userService;

@Autowired
private PermissionService permissionService;

/**
* Spring Security 加载用户信息
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名查询数据
User user = userService.selectByUsername(username);
// 判断用户是否存在
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
com.clay.common.security.entity.User curUser = new com.clay.common.security.entity.User();
BeanUtils.copyProperties(user, curUser);

// 获取用户的权限列表
List<String> permissionValueList = permissionService.selectPermissionValueByUserId(user.getId());
SecurityUser securityUser = new SecurityUser();
securityUser.setCurrentUserInfo(curUser);
securityUser.setPermissionValueList(permissionValueList);
return securityUser;
}

}

案例代码下载

完整的案例代码可以直接从 GitHub 下载对应章节 spring-security5-10