springboot+shiro+JWT 系统权限管理

做管理系统权限这块很重要,一般会通过角色来划分权限,更复杂的还可以做成数据权限,大部分的场景都是基于角色来实现的,最近项目的需求是实现后台管理系统权限控制,不同的角色有不同的权限,基于角色来实现。

认证原理

  1. 用户登陆之后,使用密码对账号进行签名生成并返回token并设置过期时间;
  2. 将token保存到本地,并且每次发送请求时都在header上携带token。
  3. shiro过滤器拦截到请求并获取header中的token,并提交到自定义realm的doGetAuthenticationInfo方法。
  4. 通过jwt解码获取token中的用户名,从数据库中查询到密码之后根据密码生成jwt效验器并对token进行验证。

表结构

首先看一下用到的表结构。

系统用户表:sys_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
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`login_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登录名' ,
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码' ,
`real_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '真实名字' ,
`last_login_time` datetime NULL DEFAULT NULL COMMENT '最后登录时间' ,
`last_login_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登录IP' ,
`status` tinyint(1) NULL DEFAULT NULL COMMENT '用户状态:0,正常 1,冻结' ,
`email` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱' ,
`mobile` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '联系电话' ,
`role_ids` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '角色id' ,
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间' ,
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间' ,
`create_by` int(11) NULL DEFAULT NULL COMMENT '更新人' ,
`update_by` int(11) NULL DEFAULT NULL COMMENT '修改人' ,
`salt` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '盐值' ,
`deleted` tinyint(1) NULL DEFAULT 0 COMMENT '删除标识:0,正常 1,删除' ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='系统用户表'
AUTO_INCREMENT=2
ROW_FORMAT=DYNAMIC
;

系统角色表:sys_role

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`role_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '角色名称' ,
`description` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述' ,
`menu_ids` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '菜单id' ,
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间' ,
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间' ,
`create_by` int(11) NULL DEFAULT NULL COMMENT '创建人' ,
`update_by` int(11) NULL DEFAULT NULL COMMENT '更新人' ,
`deleted` tinyint(1) NULL DEFAULT 0 COMMENT '删除标识:0,正常 1,删除' ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='系统角色表'
AUTO_INCREMENT=2
ROW_FORMAT=DYNAMIC
;

系统菜单表:sys_menu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE TABLE `sys_menu` (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`pid` int(11) NULL DEFAULT NULL COMMENT '菜单父id' ,
`menu_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '菜单名称' ,
`menu_url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '菜单url' ,
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间' ,
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间' ,
`create_by` int(11) NULL DEFAULT NULL COMMENT '创建人' ,
`update_by` int(11) NULL DEFAULT NULL COMMENT '更新人' ,
`deleted` tinyint(1) NULL DEFAULT 0 COMMENT '删除标识:0,正常 1,删除' ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='系统菜单表'
AUTO_INCREMENT=8
ROW_FORMAT=DYNAMIC
;

系统权限表:sys_btn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE `sys_btn` (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`btn_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限名称' ,
`btn_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限编码' ,
`btn_url` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限路径' ,
`menu_id` int(11) NULL DEFAULT NULL COMMENT '菜单id' ,
PRIMARY KEY (`id`),
INDEX `index_menu_id` (`menu_id`) USING BTREE
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='系统权限表'
AUTO_INCREMENT=35
ROW_FORMAT=DYNAMIC
;

系统授权表:sys_author

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `sys_author` (
`role_id` int(11) NULL DEFAULT NULL COMMENT '角色id' ,
`res_id` int(11) NULL DEFAULT NULL COMMENT '资源id' ,
`res_type` tinyint(1) NULL DEFAULT NULL COMMENT '资源类型'
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='系统授权表'
ROW_FORMAT=DYNAMIC
;

这是本次涉及到的表结构,表结构设计好了,相关业务也基本清楚了,下面直接集成springbootshiro,实现权限控制。

引入JAR

1
2
3
4
5
6
7
8
9
10
11
12
<!--JWT-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>

自定义JwtToken

首先我们需要自定义一个对象用来封装oken。

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
import org.apache.shiro.authc.AuthenticationToken;

/**
* 自定义一个对象用来封装token
*
* @author dgb
* @create 2018-09-30 14:14
**/

public class JwtToken implements AuthenticationToken {

private String token;

public JwtToken(String token) {
this.token = token;
}

@Override
public Object getPrincipal() {
return token;
}

@Override
public Object getCredentials() {
return token;
}
}

JwtUtil

还得一个工具类用来进行签名和效验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
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
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Date;

/**
* 用来进行签名和效验Token
*
* @author dgb
* @create 2018-09-30 14:19
**/

public class JwtUtil {

/**
* 过期时间
*/
private static final long EXPIRE_TIME = 5 * 60 * 1000;

/**
* 校验token是否正确
*
* @param token 密钥
* @param username 用户名
* @param secret 用户的密码
* @return 正确: true;不正确:false
*/
public static boolean verify(String token, String username, String secret) {
// 根据密码生成JWT校验器
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
// 校验TOKEN
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (IllegalArgumentException e) {
e.printStackTrace();
return false;
} catch (JWTVerificationException e) {
e.printStackTrace();
return false;
}
}

/**
* 获取用户名
*
* @param token token中包含了用户名
* @return
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
e.printStackTrace();
return null;
}
}

/**
* 生成签名
*
* @param username 用户名
* @param secret 密码
* @return 加密的TOKEN
*/
public static String sign(String username, String secret) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带用户信息
return JWT.create()
.withClaim("username", username)
.withExpiresAt(date)
.sign(algorithm);
}
}

ShiroFilter拦截器

在这里我们要使用shiro来拦截token,需要我们自己写一个jwt的过滤器来作为shiro的过滤器。

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 org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

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

/**
* shiro过滤器
*
* @author dgb
* @create 2018-09-30 14:36
**/

public class JwtFilter extends BasicHttpAuthenticationFilter {

/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request,
ServletResponse response, Object mappedValue) {
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 登录验证
*
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean executeLogin(ServletRequest request,
ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("Authorization");
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
}

自定义Realm

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
import club.itwork.backend.api.entity.SysUser;
import club.itwork.backend.api.service.SysUserService;
import club.itwork.common.util.StringUtil;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

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

/**
* @author dgb
* @create 2018-09-30 14:52
**/
@Component
public class ShiroRealm extends AuthorizingRealm {


@Autowired
private SysUserService sysUserService;

/**
* 必须重写此方法,不然shiro会报错
*
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}

/**
* 默认使用此方法进行用户名正确与否验证,错误抛出异常即可
*
* @param auth
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth)
throws AuthenticationException {
String token = (String) auth.getPrincipal();
String username = JwtUtil.getUsername(token);
if (StringUtil.isEmpty(username)) {
throw new IncorrectCredentialsException("用户名无效");
}

SysUser user = sysUserService.getByUsername(username);
if (user == null) {
throw new UnknownAccountException("用户不存在");
}

if(!JwtUtil.verify(token,username,user.getPassword())){
throw new AuthenticationException("密码错误");
}

return new SimpleAuthenticationInfo(token, token, getName());
}

/**
* 只有当需要检测用户权限的时候才会调用此方法
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = JwtUtil.getUsername(principals.toString());
SysUser user = sysUserService.getByUsername(username);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//根据用户名查询权限 TODO 放到redis中
List roles = new ArrayList();
roles.parallelStream().forEach(role ->{
info.addStringPermission("");
});
return info;
}
}

ShiroConfig

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
import club.itwork.backend.shiro.JwtFilter;
import club.itwork.backend.shiro.ShiroRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* @author dgb
* @create 2018-09-30 15:15
**/
@Configuration
public class ShiroConfig {

@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
//拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 配置不会拦截的链接,顺序判断
filterChainDefinitionMap.put("/login/**", "anon");
filterChainDefinitionMap.put("/**.js", "anon");
filterChainDefinitionMap.put("/swagger**/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<>(1);
filterMap.put("jwt", new JwtFilter());
factoryBean.setFilters(filterMap);
// 过滤链接定义,从上向下顺序执行,一般将/**放在最为下边
filterChainDefinitionMap.put("/**", "jwt");
//未授权界面;
factoryBean.setUnauthorizedUrl("/403");
factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return factoryBean;
}

@Bean("securityManager")
public SecurityManager securityManager(ShiroRealm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);

// 关闭shiro自带的session,详情见文档
// http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
evaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(evaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}

/**
* 下面的代码是添加注解支持
*
* @return
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
creator.setProxyTargetClass(true);
return creator;
}

@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}

@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}

登录Demo

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
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;

/**
* @author dgb
* @create 2018-10-08 9:06
**/

@RestController
public class LoginController {

@PostMapping("/login")
public RespData login(String username, String password, String code) {
try {
Subject subject = SecurityUtils.getSubject();
String token = JwtUtil.sign(username, MD5Util.encode(password, "UTF-8", false));
JwtToken jwtToken = new JwtToken(token);
subject.login(jwtToken);
return RespData.successMsg("登录成功");
} catch (UnknownAccountException ex) {
return RespData.errorMsg("用户不存在!");
} catch (IncorrectCredentialsException ex) {
return RespData.errorMsg("用户名无效!");
} catch (AuthenticationException ae) {
return RespData.errorMsg("密码错误!");
}
}


@RequestMapping(value = "/validateCode")
public String validateCode(HttpServletResponse response) {
// 设置响应的类型格式为图片格式
response.setContentType("image/jpeg");
//禁止图像缓存。
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);

ValidateCodeUtil code = new ValidateCodeUtil(110, 34, 4, 50);
//SessionUtils.getSession().setAttribute(SessionUtils.IMG_CODE,code.getCode());
try {
code.write(response.getOutputStream());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

这是我本次项目中springboot集成shiro的过程,具体配置还得根据自己的项目结构集成。

使用JWT的好处

  • 简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快。
  • 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库。
  • 安全(security): 与简单的JSON相比,XML和XML数字签名会引入复杂的安全漏洞。

参考:springboot2+shiro+jwt整合

获取源码

关注公众号「特想学英语」,回复 shiro-jwt

原文作者: dgb8901,yinxing

原文链接: https://www.itwork.club/2018/10/08/springboot-shiro-jwt/

版权声明: 转载请注明出处

为您推荐

体验小程序「简易记账」

关注公众号「特想学英语」

CSS学习: 选择器