SpringBoot 2.x+Shiro+Redis

一直想好好的学习一下安全方面的框架,自己对于这个方面的知识很欠缺,借助当前公司项目的机会,认真的研究了两天Shiro这个高度可定制的框架(虽然在Spring Boot中集成Spring Security更为方便,但是看了一天的相关资料,感觉还是Shiro更简单,以后再认真学习Spring Security)。

项目数据库使用的是MongoDB,稍后使用MySQL做一个集成,毕竟用到MongoDB的不多,其实都是大同小异,不过在SpringBoot中使用MongoDB特别方便。

1 准备

  • Spring Boot 2.x
  • Shiro
  • Redis
  • Maven 3.6
  • JDK 1.8
  • IDEA

2 Shiro介绍

直达官网:http://shiro.apache.org

image.png

  • Shiro中四大模块如图:
    • Authentication,身份证认证,一般就是登录
    • Authorization,授权,给用户分配角色或者访问某些资源的权限
    • Session Management, 用户的会话管理员,多数情况下是web session
    • Cryptography, 数据加解密,比如密码加解密等

Shiro权限控制流程及相关概念(http://shiro.apache.org/architecture.html),其中:

  • Subject:主体,如用户或程序,主体去访问系统或者资源
  • SecurityManager:安全管理器,Subject的认证和授权都需在安全管理器下进行
  • Authenticator:认证器,主要负责Subject的认证
  • Realm:数据域,Shiro和安全数据的连接器,类似于jdbc连接数据库; 通过realm获取认证授权相关信息,在集成时这个部分需要重写Shiro指定的方法
  • Authorizer:授权器,主要负责Subject的授权, 控制subject拥有的角色或者权限
  • Cryptography:加解密,Shiro中包含易于使用和理解的数据加解密方法,简化了很多复杂的api
  • Cache Manager:缓存管理器,比如认证或授权信息,通过缓存进行管理,提高性能

3 集成Shiro

3.1 数据库设计

image.png

3.2 创建项目

使用IDEA创建一个空的Spring Boot项目,勾选相关web依赖和数据库依赖,这里把相关依赖全部罗列出来

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
<properties>
<java.version>1.8</java.version>
<shiro-spring.version>1.5.1</shiro-spring.version>
<shiro-redis.version>3.2.3</shiro-redis.version>
<druid.version>1.1.20</druid.version>
<fastjson.version>1.2.67</fastjson.version>
<mybatis-plus.version>3.3.0</mybatis-plus.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- FastJson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- druid数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!--整合shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro-spring.version}</version>
</dependency>
<!--整合shiro-redis缓存插件-->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>${shiro-redis.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

3.3 配置application.yml文件

配置数据库连接相关信息

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
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_role?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: db_role
password: db_role

redis:
database: 1 # Redis数据库索引(默认为0)
host: 192.168.213.146 # Redis服务器地址
port: 6379 # Redis服务器连接端口
password: coctrl # Redis服务器连接密码(默认为空)
timeout: 0 # 连接超时时间(毫秒)
jedis:
pool:
max-active: 8
max-idle: 8
max-wait: -1ms
min-idle: 0

# SQL打印
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

3.4 集成mybatis-plus

3.4.1 entity

实体类与数据库一一对应就行,可以配合mybatis-plus直接自动生成

3.4.2 mapper

  • UserMapper.java

只写了一个用于登录是通过用户名查对应信息的方法

1
2
3
4
5
6
7
8
9
@Component
public interface UserMapper extends BaseMapper<User> {
/**
* 根据用户名查询用户信息,登录
* @param username
* @return
*/
UserVO findUserInfoByUsername(String username);
}

UserVO是一个自定义的实体类,用于封装用户、角色、权限信息,具体可以看项目源码

  • UserMapper.xml
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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kangaroohy.shiroredis.mapper.UserMapper">

<resultMap id="userInfoMap" type="com.kangaroohy.shiroredis.domain.entity.vo.UserVO" >
<id property="id" column="u_id" />
<result property="username" column="u_username"/>
<result property="password" column="u_password"/>
<result property="gender" column="u_gender"/>
<result property="updateTime" column="u_updateTime"/>
<collection property="roleList" ofType="com.kangaroohy.shiroredis.domain.entity.vo.RoleVO" >
<id property="role.id" column="r_id" />
<result property="role.name" column="r_name"/>
<result property="role.description" column="r_description"/>
<collection property="permissionList" ofType="com.kangaroohy.shiroredis.domain.entity.po.Permission" >
<id property="id" column="p_id" />
<result property="name" column="p_name"/>
<result property="url" column="p_url"/>
</collection>
</collection>
</resultMap>

<select id="findUserInfoByUsername" resultMap="userInfoMap">
SELECT
u.id u_id,
u.username u_username,
u.password u_password,
u.gender u_gender,
u.update_time u_updateTime,
r.id r_id,
r.name r_name,
r.description r_description,
p.id p_id,
p.name p_name,
p.url p_url
FROM
t_user u
LEFT JOIN t_user_role ur ON ur.uid = u.id
LEFT JOIN t_role r ON r.id = ur.rid
LEFT JOIN t_role_permission rp ON rp.rid = r.id
LEFT JOIN t_permission p ON p.id = rp.pid
WHERE
u.username = #{username}
</select>
</mapper>

3.4.3 service

这个地方就是一个简单的调用mapper中的方法,具体可以看源码

不要忘记启动器类配置mybatis的mapper扫描路径,这个集成mybatis-plus的时候应该有配置

1
@MapperScan("com.kangaroohy.shiroredis.mapper")

3.5 配置Shiro

3.5.1 自定义Realm:CustomRealm.java

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
@Slf4j
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserService userService;

/**
* 授权时调用(权限校验)
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获得当前对象
UserVO user = (UserVO) principalCollection.getPrimaryPrincipal();

log.info("用户 {} 调用了 doGetAuthorizationInfo 进行授权", user.getUsername());

UserVO info = userService.findUserInfoByUsername(user.getUsername());

Set<String> stringRolesList = new HashSet<>();
Set<String> stringPermissionsList = new HashSet<>();

List<RoleVO> roleList = info.getRoleList();
for (RoleVO role : roleList) {
stringRolesList.add(role.getRole().getName());
List<Permission> permissionList = role.getPermissionList();
for (Permission permission : permissionList){
if (permission != null){
stringPermissionsList.add(permission.getName());
}
}
}

SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRoles(stringRolesList);
simpleAuthorizationInfo.addStringPermissions(stringPermissionsList);
return simpleAuthorizationInfo;
}

/**
* 登录时调用
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//从token中获得用户信息,这个token是用户的输入信息
String username = (String) authenticationToken.getPrincipal();
UserVO info = userService.findUserInfoByUsername(username);

//取密码
String password = info.getPassword();
if (password == null || "".equals(password)) {
return null;
}
//设置盐值,使用账号作为盐值
ByteSource salt = ByteSource.Util.bytes(username);
//参数一传入对象,便于全局获取当前对象信息
return new SimpleAuthenticationInfo(info, password, salt, getName());
}
}

Realm能做的工作主要有以下几个方面:

  • 验证是否能登录,并返回验证信息(getAuthenticationInfo方法)
  • 验证是否有访问指定资源的权限,并返回所拥有的所有权限(getAuthorizationInfo方法)
  • 判断是否支持token(例如:HostAuthenticationToken,UsernamePasswordToken等)(supports方法)

自定义Realm中实现的是前两个

3.5.2 自定义SessionManager:CustomSessionManager.java

将生成的sessionId作为认证时的token,登录之后的每次请求,header中必须携带token参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CustomSessionManager extends DefaultWebSessionManager {

private static final String AUTHORIZATION = "token";

public CustomSessionManager() {
super();
}

@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String sessionId = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
if (sessionId != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return sessionId;
} else {
return super.getSessionId(request, response);
}
}
}

3.5.3 自定义sessionId生成器:CustomSessionId.java

1
2
3
4
5
6
public class CustomSessionId implements SessionIdGenerator {
@Override
public Serializable generateId(Session session) {
return "kangaroohy" + UUID.randomUUID().toString().replace("-", "");
}
}

3.5.4 shiro配置文件:ShiroConfig.java

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
@Configuration
public class ShiroConfig {

@Value("${spring.redis.database}")
private Integer database;

@Value("${spring.redis.host}")
private String host;

@Value("${spring.redis.port}")
private Integer port;

@Value("${spring.redis.password}")
private String password;

@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

//设置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//需要登陆的接口,没有登陆就访问需要登陆的接口,则调用这个接口(非前后端分离,则调用登录界面)
shiroFilterFactoryBean.setLoginUrl("/pub/need_login");
//登录成功,跳转url,前后端分离的情况下,则没有这个接口的调用
shiroFilterFactoryBean.setSuccessUrl("/");
//已经登陆,但是访问的接口没有权限,类似403界面
shiroFilterFactoryBean.setUnauthorizedUrl("/pub/unauthorized");

//自定义退出后重定向的地址,前后端分离,用于返回退出成功信息
LogoutFilter logout = new LogoutFilter();
logout.setRedirectUrl("/pub/logout");

//设置自定义filter
Map<String, Filter> filterMap = new LinkedHashMap<>();
//自定义退出filter
filterMap.put("logout",logout);
shiroFilterFactoryBean.setFilters(filterMap);

//拦截器路径,务必设置为LinkedHashMap,否则部分路径拦截时有时无(不生效),因为如果使用HashMap,无序,而LinkedHashMap,有序
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//退出过滤器,退出成功后,默认返回LoginUrl接口,即登录页,通过自定义,改到/pub/logout接口
filterChainDefinitionMap.put("/logout", "logout");
//匿名可访问
filterChainDefinitionMap.put("/pub/**", "anon");
//登录后可以访问
filterChainDefinitionMap.put("/user/**", "authc");
//通过认证才能访问
filterChainDefinitionMap.put("/**", "authc");

shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}

@Bean(name = "securityManager")
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//如果不是前后端分离,不用设置这个
securityManager.setSessionManager(sessionManager());
//设置自定义的cacheManager
securityManager.setCacheManager(cacheManager());
//设置realm(推荐放到最后)
securityManager.setRealm(customRealm());
return securityManager;
}

/**
* 配置具体的cache实现类
*/
public org.crazycake.shiro.RedisCacheManager cacheManager(){
org.crazycake.shiro.RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
//过期时间,单位 s
redisCacheManager.setExpire(Constant.CACHE_EXPIRE_TIME);
redisCacheManager.setPrincipalIdFieldName(Constant.ACCOUNT);
redisCacheManager.setKeyPrefix(Constant.ACTIVE_SHIRO_CACHE);
return redisCacheManager;
}

/**
* 自定义realm
* @return
*/
@Bean(name = "customRealm")
public CustomRealm customRealm() {
CustomRealm realm = new CustomRealm();
realm.setCredentialsMatcher(credentialsMatcher());
return realm;
}

/**
* 密码加解密规则设置
* @return
*/
@Bean(name = "credentialsMatcher")
public HashedCredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置散列算法,这里使用MD5
credentialsMatcher.setHashAlgorithmName(Constant.HASH_NAME);
//散列次数
credentialsMatcher.setHashIterations(Constant.HASH_TIME);
return credentialsMatcher;
}

@Bean(name = "sessionManager")
public SessionManager sessionManager(){
CustomSessionManager sessionManager = new CustomSessionManager();
//过期时间,默认30分钟超时,方法中单位是毫秒,此处15分钟过期:15 * 60 * 1000
sessionManager.setGlobalSessionTimeout(Constant.SESSION_EXPIRE_TIME);
//配置session持久化
sessionManager.setSessionDAO(redisSessionDAO());
sessionManager.setSessionIdCookieEnabled(false);
sessionManager.setSessionIdUrlRewritingEnabled(false);
sessionManager.setDeleteInvalidSessions(true);
return sessionManager;
}

/**
* 自定义Session持久化
* @return
*/
public RedisSessionDAO redisSessionDAO(){
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
redisSessionDAO.setSessionIdGenerator(new CustomSessionId());
redisSessionDAO.setKeyPrefix(Constant.ACTIVE_SHIRO_SESSION);
return redisSessionDAO;
}

/**
* 配置redisManager
*/
public RedisManager redisManager(){
RedisManager redisManager = new RedisManager();
redisManager.setDatabase(database);
//在shiro-redis 3.2.3版本中,host合并了port
redisManager.setHost(host + ":" + port);
//redisManager.setPort(port);
redisManager.setPassword(password);
return redisManager;
}

/**
* 管理shiro一些Bean的生命周期,初始化和销毁
* @return
*/
@Bean(name = "lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}

/**
* 使注解生效,不加这个,shrio的AOP注解不生效
* 如 Controller中 shiro的 @RequiresGust(游客可以访问) 注解
* @return
*/
@Bean(name = "authorizationAttributeSourceAdvisor")
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
AuthorizationAttributeSourceAdvisor sourceAdvisor = new AuthorizationAttributeSourceAdvisor();
sourceAdvisor.setSecurityManager(securityManager());
return sourceAdvisor;
}

/**
* 用来扫描上下文,寻找所有的Advisor(通知器),将符合条件的Advisor应用到切入点的Bean中
* 需要在LifecycleBeanPostProcessor创建之后才可以创建
* @return
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
// https://zhuanlan.zhihu.com/p/29161098
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
}

3.5.5 登录用到的controller:PublicController.java

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
@RestController
@RequestMapping("/pub")
@Slf4j
public class PublicController {

UserService userService;

public PublicController(UserService userService) {
this.userService = userService;
}

@GetMapping("/need_login")
public RestResult<String> needLogin() {
return RestResult.error(RestCode.NEED_LOGIN_ERROR);
}

@GetMapping("/unauthorized")
public RestResult<String> unauthorized() {
return RestResult.error(RestCode.AUTHORIZATION_ERROR);
}

@PostMapping("/login")
public RestResult<UserVO> login(@RequestParam String username, @RequestParam String password, HttpServletResponse response) {
Subject subject = SecurityUtils.getSubject();
//踢出之前登录状态,同一账号 同一时间 只能在同一个地方登录
kickOutBefore(username.trim());

UserVO userInfo = userService.findUserInfoByUsername(username.trim());
//用户名查询用户
if (userInfo == null) {
return RestResult.error("账号不存在");
}
try {
UsernamePasswordToken userPwdToken = new UsernamePasswordToken(username.trim(), password.trim());
subject.login(userPwdToken);
String token = subject.getSession().getId().toString();
UserVO info = userService.findUserInfoByUsername(username.trim());
info.setPassword(null);
JedisUtil.setJson(Constant.ACTIVE_SHIRO_TOKEN + username.trim(), token, Constant.ONLINE_TOKEN_EXPIRE_TIME);
response.setHeader(Constant.AUTHORIZATION, token);
response.setHeader("Access-Control-Expose-Headers", Constant.AUTHORIZATION);
return RestResult.ok(info);
} catch (Exception e) {
e.printStackTrace();
return RestResult.error("用户名或密码错误");
}
}

/**
* 根据userId踢出 同一用户 不同浏览器登录session
* @param username 账号
*/
private void kickOutBefore(String username) {
String key = Constant.ACTIVE_SHIRO_TOKEN + username.trim();
if (JedisUtil.exists(key)) {
String value = JedisUtil.getJson(key);
if (JedisUtil.exists(Constant.ACTIVE_SHIRO_SESSION + value)) {
JedisUtil.delete(key, Constant.ACTIVE_SHIRO_SESSION + value);
} else {
JedisUtil.delete(key);
}
}
}
}

3.6 返回JSON数据的自定义类

  • RestCode.java
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
public enum RestCode {
//运行时错误
UNKNOWN_ERROR(-1, "Unknown Error! Contact the administrator if this problem continues."),
//操作成功
SUCCESS(200, "SUCCESS"),
//请求参数错误、不合法
PARAM_ERROR(400, "Illegal Parameter!"),
//需要登录才能访问
NEED_LOGIN_ERROR(401, "Please login and visit again!"),
//没有访问权限,禁止访问
AUTHORIZATION_ERROR(403, "You are forbidden to view it!"),
//资源不存在
NOTFOUND_ERROR(404, "Not Found"),
//方法不支持
NOT_SUPPORT_ERROR(500, "Not support this method");

private Integer code;
private String message;

RestCode(){
}

RestCode(Integer code, String message) {
this.code = code;
this.message = message;
}

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}
  • RestResult.java
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
@Data
public class RestResult<T> implements Serializable {

private static final long serialVersionUID = 7711799662216684129L;

@JSONField(ordinal = 1)
public int code;

@JSONField(ordinal = 2)
private String msg;

@JSONField(ordinal = 3)
private T data;

public RestResult() {
}

public RestResult(int code, String msg) {
this.code = code;
this.msg = msg;
}

public RestResult(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}

public static <T> RestResult<T> ok() {
return new RestResult<>(RestCode.SUCCESS.getCode(), RestCode.SUCCESS.getMessage());
}

public static <T> RestResult<T> ok(String msg) {
return new RestResult<>(RestCode.SUCCESS.getCode(), msg);
}

public static <T> RestResult<T> ok(T data) {
return new RestResult<>(RestCode.SUCCESS.getCode(), RestCode.SUCCESS.getMessage(), data);
}

public static <T> RestResult<T> error(String msg) {
return new RestResult<>(RestCode.UNKNOWN_ERROR.getCode(), msg);
}

public static <T> RestResult<T> error(RestCode restCode) {
return new RestResult<>(restCode.getCode(), restCode.getMessage());
}

public static <T> RestResult<T> error(RestCode restCode, String msg) {
return new RestResult<>(restCode.getCode(), msg);
}

public static <T> RestResult<T> error(RestCode restCode, String msg, T data) {
return new RestResult<>(restCode.getCode(), msg, data);
}
}

4 Demo下载

无Redis缓存版本:https://github.com/kangaroo1122/ssm_shiro

有Redis缓存版本:https://github.com/kangaroo1122/shiro-redis

两个版本都可以直接下载,按照给的数据库设计,创建数据库,修改数据库地址即可运行。

用户密码加密方式:可以查看自定义Realm,这样就能自己修改数据库用户数据了

其他

关于redis的配置文件和操作redis数据库的util,可以直接查看demo源码,Redis版本还集成了mybatis-plus枚举类型转换器,具体可以看mybatis-plus的官方介绍.