Shiro 简介
Shiro 是一个Java安全框架,用于身份验证、授权、加密和会话管理
官方主页 [https://shiro.apache.org/]{https://shiro.apache.org/}
SpringBoot 集成 Shiro
引入依赖
1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.8.0</version>
</dependency>
Realm 设置
要想自定义实现身份认证和授权功能,就必须继承 org.apache.shiro.authc.AuthenticationException
类,实现自定义的认证 Realm
doGetAuthenticationInfo() 用于登录认证,可以在此处实现用户登录时的认证方式,我是用 邮箱+密码 的方式直接去数据库里找进行验证,这里的参数 token 就是在登录的时候传入的,具体见下文。
doGetAuthorizationInfo() 用于实现获取用户的角色和权限,在执行带有
@RequiresPermissions
或@RequiresRoles
的方法前、执行subject.hasPermissions()
或subjwect.hasRoles()
方法时会执行此方法
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
@Component
public class ShiroRealm extends AuthorizingRealm {
@Resource
private UserService userService;
@Resource
private CacheManager cacheManager;
@PostConstruct
private void initConfig() {
setAuthenticationCachingEnabled(false);
setAuthorizationCachingEnabled(true);
setCachingEnabled(true);
setCacheManager(cacheManager);
}
/**
* 授权模块,获取用户角色和权限
*
* @param principal principal
* @return AuthorizationInfo 权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
User user = (User) principal.getPrimaryPrincipal();
user = userService.doGetUserAuthorization(user);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setRoles(user.getRoleNames());
simpleAuthorizationInfo.setStringPermissions(user.getPermissions());
return simpleAuthorizationInfo;
}
/**
* 用户认证
*
* @param token AuthenticationToken 身份认证 token
* @return AuthenticationInfo 身份认证信息
* @throws AuthenticationException 认证相关异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取用户输入的邮箱和密码
String email = (String) token.getPrincipal();
String password = new String((char[]) token.getCredentials());
// 通过用户名到数据库查询用户信息
User user = this.userService.findByEmail(email);
if (user == null || !StringUtils.isEquals(password, user.getPassword())) {
throw new IncorrectCredentialsException("邮箱或密码错误!");
}
if (UserStatusEnum.LOCK.getCode().equals(user.getStatus())) {
throw new LockedAccountException("账号已被锁定,请联系客服!");
}
return new SimpleAuthenticationInfo(user, password, getName());
}
}
ShiroConfig
新建一个配置类,用于添加 Shiro 用到的所有的 bean
首先是一堆写在 spring.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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.oes.common.constans.OesConstant;
import org.oes.common.constans.Strings;
import org.oes.common.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.Base64Utils;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
@Configuration(proxyBeanMethods = false)
public class ShiroConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.database:0}")
private int database;
@Value("${oes.shiro.session-timeout}")
private long shiroSessionTimeout;
@Value("${oes.shiro.cookie-timeout}")
private int shiroCookieTimeout;
// ... 方法见下文
}
对应的配置文件新增内容
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
spring:
redis:
# Redis数据库索引(默认为 0)
database: 0
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis 密码
password:
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 8
# 连接池中的最大空闲连接
max-idle: 500
# 连接池最大连接数(使用负值表示没有限制)
max-active: 2000
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 10000
# 连接超时时间(毫秒)
timeout: 5000
# 系统常量配置
oes:
shiro:
session-timeout: 3600 # session 超时时间,单位为秒
cookie-timeout: 864000 # rememberMe cookie有效时长,单位为秒
首先是一个 Redis 的管理器,用于登录 Redis 服务,进行存取操作,此处用到了,前五个配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* shiro 中配置 redis 缓存
*
* @return RedisManager
*/
private RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host + Strings.COLON + port);
if (StringUtils.isNotBlank(password)) {
redisManager.setPassword(password);
}
redisManager.setTimeout(timeout);
redisManager.setDatabase(database);
return redisManager;
}
然后是缓存管理,我们直接使用 Redis 作为缓存管理器,创建一个缓存管理器的 bean
1
2
3
4
5
6
7
8
9
@Bean
public CacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setExpire((int) shiroSessionTimeout);
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
然后是关于 Cookie 用户设置设置记住我的功能
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
/**
* cookie管理对象
*
* @return CookieRememberMeManager
*/
private CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
// rememberMe cookie 加密的密钥
String encryptKey = "KEY_OES";
byte[] encryptKeyBytes = encryptKey.getBytes(StandardCharsets.UTF_8);
String rememberKey = Base64Utils.encodeToString(Arrays.copyOf(encryptKeyBytes, 16));
cookieRememberMeManager.setCipherKey(Base64.decode(rememberKey));
return cookieRememberMeManager;
}
/**
* rememberMe cookie 效果是重开浏览器后无需重新登录
*
* @return SimpleCookie
*/
private SimpleCookie rememberMeCookie() {
// 设置 cookie 名称,对应 login.html 页面的 <input type="checkbox" name="rememberMe"/>
SimpleCookie cookie = new SimpleCookie("rememberMe");
// 设置 cookie 的过期时间,单位为秒,这里为一天
cookie.setMaxAge(shiroCookieTimeout);
return cookie;
}
然后是会话管理器,设置 Shiro 所使用的会话管理的管理器对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* session 管理对象
*
* @return DefaultWebSessionManager
*/
@Bean
public SessionManager sessionManager(SessionDAO sessionDAO) {
ShiroSessionManager sessionManager = new DefaultWebSessionManager();
// 设置 session超时时间
sessionManager.setGlobalSessionTimeout(shiroSessionTimeout * 1000L);
sessionManager.setSessionDAO(sessionDAO);
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}
@Bean
public SessionDAO sessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
最后是 securityManager,这是 Shiro 框架下的核心管理器,Shiro 所有的授权认证行为都将由此管理,在这里设置了认证使用的Realm、会话管理器、缓存管理器、记住我的Cookie
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm,
SessionManager sessionManager,
CacheManager cacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 配置 SecurityManager,并注入 shiroRealm
securityManager.setRealm(shiroRealm);
// 配置 shiro session管理器
securityManager.setSessionManager(sessionManager);
// 配置 缓存管理类 cacheManager
securityManager.setCacheManager(cacheManager);
// 配置 rememberMeCookie
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
ShiroEarlyConfig
这个类其实可以和 ShiroConfig 合并,因为功能是一样的,但是为了减少对Bean后置处理器的影响,把它独立了出来,它主要实现了一个工厂 bean 创建 ShiroFilterFactoryBean
此处还有一个坑 DefaultAdvisorAutoProxyCreator
bean在我的系统中Spring并不会自动创建,需要手动创建,否则 Shiro 中的 @RequiresPermissions 注解会失效
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 org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.oes.common.constans.URIs;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import java.util.LinkedHashMap;
import java.util.stream.Collectors;
/**
* 将注册时机较早的Bean单独提取出来,并且相关依赖延迟注入,
* 尽可能的缩小对Bean后置处理器的影响
* <p>
* https://github.com/spring-projects/spring-boot/issues/16097
* https://issues.apache.org/jira/browse/SHIRO-743
*/
@Configuration(proxyBeanMethods = false)
public class ShiroEarlyConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Lazy DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置 securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 登录的 url
shiroFilterFactoryBean.setLoginUrl(URIs.LOGIN);
// 登录成功后跳转的 url
shiroFilterFactoryBean.setSuccessUrl(URIs.SUCCESS);
// 未授权 url
shiroFilterFactoryBean.setUnauthorizedUrl(URIs.UNAUTHORIZED);
LinkedHashMap<String, String> filterChainDefinitionMap = URIs.unauthorized.stream()
.collect(Collectors.toMap(url -> url, url -> "anon", (a, b) -> b, LinkedHashMap::new));
filterChainDefinitionMap.put(URIs.LOGOUT, "logout");
// 除登出以外所有 url都必须认证通过才可以访问,未通过认证自动访问 LoginUrl
filterChainDefinitionMap.put(URIs.ALL, "user");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/*
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
*/
/**
* 配置此对象的目的是,在spring容器启动时,
* 扫描所有的advisor(顾问)对象,基于advisor
* 对象中切入点的描述,为目标对象创建代理对象
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Lazy DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
关于后置处理器,启动时会有如下的日志,虽然只是一个INFO,可能不会影响到系统功能
1
[main] INFO o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker - Bean 'shiroEarlyConfig' of type [org.oes.start.tools.shiro.ShiroEarlyConfig] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
权限问题
在上一节的 ShiroFilterFactoryBean
bean 创建的时候有配置一些对 URI 访问时的权限认证
LoginUrl:判断用户未登录时跳转的地址
successUrl:登陆成功跳转 URL
UnauthorizedUrl:未授权的 URL
其他 URL 及其使用的过滤器,具体解释可参见 http://www.cppblog.com/guojingjia2006/archive/2014/05/14/206956.html
使用的 URIs 常量类如下
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
package org.oes.common.constans;
import java.util.ArrayList;
import java.util.List;
/**
* 资源地址
*/
public class URIs {
public static final String ALL = "/**";
/**
* 登录
*/
public static final String LOGIN = "/login";
/**
* 注册
*/
public static final String REGISTER = "/register";
/**
* 忘记密码
*/
public static final String FORGET = "/forget";
/**
* 验证手机验证码
*/
public static final String PHONE_VERIFICATION = "/phone";
/**
* 验证邮箱验证码
*/
public static final String EMAIL_VERIFICATION = "/email";
/**
* 设置密码
*/
public static final String PASSWORD = "/password";
/**
* 未授权
*/
public static final String UNAUTHORIZED = "/unauthorized";
/**
* 登出
*/
public static final String LOGOUT = "/logout";
/**
* 登录成功
*/
public static final String SUCCESS = "/success";
/**
* 角色操作
*/
public static final String ROLE = "/role";
/**
* 权限操作
*/
public static final String PERMISSIONS = "/permissions";
/**
* 用户操作
*/
public static final String USER = "/user";
/**
* 课程操作
*/
public static final String COURSE = "/course";
/**
* 免认证部分的URL
*/
public static List<String> unauthorized = new ArrayList<>();
static {
unauthorized.add(LOGIN);
unauthorized.add(REGISTER);
unauthorized.add(PHONE_VERIFICATION);
unauthorized.add(EMAIL_VERIFICATION);
unauthorized.add(UNAUTHORIZED);
unauthorized.add(SUCCESS);
}
}
权限使用
我仅使用到了 权限过滤 功能,角色过滤同理
1
2
3
4
5
@RequestMapping(path = URIs.TEST, method = RequestMethod.GET)
@RequiresPermissions("perms:test")
public OesHttpResponse test() {
return OesHttpResponse.getSuccess();
}
访问成功则返回 success,否则会跳转到对应的地址
参考
https://blog.csdn.net/xiaoxiaole0313/article/details/105501799
https://blog.csdn.net/weixin_46504244/article/details/120385617