shiro
视频地址:https://www.bilibili.com/video/BV1uz4y197Zm
# 一、权限相当的概念
# 认证
Authentication:用户身份识别,通常被称为用户“登录”
# 授权
Authorization:访问控制。比如某个用户是否具有某个操作的使用权限。
# 二、 Shiro及Shiro认证
官方文档: http://shiro.apache.org/documentation.html
源码下载
git clone https://github.com.cnpmjs.org/apache/shiro.git
git checkout shiro-root-1.8.0 -b shiro-root-1.8.0 #切换到最新的1.8分支上
2
Subject
:主体,可以看到主体可以是任何可以与应用交互的 “用户”;
SecurityManager
:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
Authenticator
:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authorizer
:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro 不知道你的用户 / 权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm;
SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所以呢,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台 Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器);
SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;
CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密 / 解密的。
# Shiro 身份验证
# 身份验证
身份验证,即在应用中谁能证明他就是他本人。一般提供如他们的身份 ID 一些标识信息来表明他就是他本人,如提供身份证,用户名 / 密码来证明。
在 shiro 中,用户需要提供principals
(身份)和credentials
(证明)给 shiro,从而应用能验证用户身份:
principals
:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名 / 密码 / 手机号。
credentials
:证明 / 凭证,即只有主体知道的安全值,如密码 / 数字证书等。
最常见的 principals 和 credentials 组合就是用户名 / 密码了。接下来先进行一个基本的身份认证。
另外两个相关的概念是之前提到的 Subject
及 Realm
,分别是主体及验证主体的数据源。
简单理解: Subject为用户, pricipals 为用户名 credentials 为密码
# 身份认证流程
流程如下:
- 首先调用 Subject.login(token) 进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils.setSecurityManager() 设置;
- SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
- Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
- Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
- Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回 / 抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。
# shiro配置文件
.ini (支持复杂数据格式),用来学习shiro书写我们系统中相关权限数据,Springboot中用不到
[users]
zhang=123 wang=123
2
此处使用 ini 配置文件,通过 [users] 指定了两个主体:zhang/123、wang/123。
/**
* MD5算法
*
* 1、不可逆,一般用作 加密 或 签名
* 2、如果内容相同,无论执行多少次MD5算法生成结果都是一样的
*
* 生成结果:始终是一个16进制的长孺为32位的字符串
*
* @author passerbyYSQ
* @create 2020-08-20 20:13
*/
public class Demo {
public static void main(String[] args) {
// ctrl + d 复制当前行
// ctrl + y 删除当前行
// ctrl + alt + l 格式化代码
// 1. 全局的安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
CustomizeMd5Realm realm = new CustomizeMd5Realm();
//2. 设置 Realm 使用MD5算法的哈希凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");
credentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(credentialsMatcher);
// 设置 Realm(类似数据源)
// securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
// securityManager.setRealm(new CustomizeRealm());
securityManager.setRealm(realm);
// 3.全局的安全工具类,设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 4.拿到主体
Subject subject = SecurityUtils.getSubject();
//5. 创建令牌,模拟主体携带令牌访问
UsernamePasswordToken token = new UsernamePasswordToken("ysq", "123");
// 调用主体的方法,进行 认证
// 认证不通过抛出对应异常
// try - catch 快捷键 ctrl + alt + t
try {
// 是否已经认证
System.out.println(subject.isAuthenticated());
subject.login(token);
System.out.println("登录成功: " + subject.isAuthenticated());
} catch (UnknownAccountException e1) {
// username 错误,会抛出 UnknownAccountException 异常
e1.printStackTrace();
System.out.println("用户名错误");
} catch (IncorrectCredentialsException e2) {
// password 错误,会抛出 IncorrectCredentialsException 异常
// Credential: 凭证
e2.printStackTrace();
System.out.println("密码错误");
}
// catch (AuthenticationException e) {
// e.printStackTrace();
// }
// 如果主体已经认证
if (subject.isAuthenticated()) {
// 基于单角色的权限控制
// System.out.println(subject.hasRole("admin"));
// 基于多角色的权限控制
// 是否【同时】具有多种角色
// System.out.println(subject.hasAllRoles(Arrays.asList("admin", "user")));
// 判断是否具有对应角色,返回一个boolean数组
// boolean[] hasRole = subject.hasRoles(Arrays.asList("admin", "user", "super"));
// for (boolean b : hasRole) {
// System.out.println(b);
// }
// 基于权限字符串的访问控制
// 资源标识符:操作:资源类型
subject.isPermitted("user:update:01");
// 判断是否具有对应权限
// 判断是否【同时】具有多个权限
// subject.isPermittedAll()
}
}
}
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
# 相关类:
DefaultSecurityManager
IniRealm
SecurityUtils
Subject
UsernamePasswordToken
异常:UnknownAccountException IncorrectCredentialsException
AuthenticationException
DisabledAccountException
(帐号被禁用)
LockedAccountException
(帐号被锁定)
ExcessiveAttemptsException
(登录失败次数过多)
ExpiredCredentialsException
(凭证过期)
# 源码分析:
入口 方法 subject.login(token); DelegatingSubject
--> securityManager.login(this, token);
-->
AuthenticatingSecurityManager
AbstractAuthenticator
ModularRealmAuthenticator
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
2
3
4
5
6
7
8
9
--> AuthenticationInfo info = realm.getAuthenticationInfo(token); AuthenticatingRealm
--> SimpleAccountRealm
SimpleAccount
// SimpleAccountRealm 认证的源码
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
SimpleAccount account = getUser(upToken.getUsername());
if (account != null) {
if (account.isLocked()) {
throw new LockedAccountException("Account [" + account + "] is locked.");
}
if (account.isCredentialsExpired()) {
String msg = "The credentials for account [" + account + "] are expired";
throw new ExpiredCredentialsException(msg);
}
}
return account;
}
// 授权的源码
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = getUsername(principals);
USERS_LOCK.readLock().lock();
try {
return this.users.get(username);
} finally {
USERS_LOCK.readLock().unlock();
}
}
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
认证:
最终执行用户名比较 ,在SimpleAccountRealm
的 doGetAuthenticationInfo()
方法里面;
执行密码校验:AuthenticatingRealm
assertCredentialsMatch()
方法 ( 调用CredentialsMatcher
doCredentialsMatch()
),由系统自动进行处理
自定义认证最核心的就是要自定义Realm, 参考 SimpleAccountRealm
AuthenticatingRealm
处理认证 AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
AuthorizingRealm
处理授权 AuthenticationInfo doGetAuthorizationInfo(PrincipalCollection principals)
# 自定义Realm
参考 SimpleAccountRealm
,将认证、授权数据的来源转为数据库的实现
/**
* 自定义 Realm *
* 将 认证 或 授权 的数据来源从 .ini 转到 数据库
*/
public class CustomizeRealm extends AuthorizingRealm {
/**
* Authorization 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* Authentication 认证
* @param authenticationToken
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 在 token 种获取 username(身份信息)
// principal 主角
String principal = (String) authenticationToken.getPrincipal();
System.out.println(principal);
// 根据身份信息,通过 jdbc 从数据查询出 password
if ("zs".equalsIgnoreCase(principal)) {
// credential: 凭据、凭证(正确的,从数据库读取出来的)
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
principal, "123", this.getName()
);
return authenticationInfo;
}
return null;
}
}
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
相关类:
SimpleAuthenticationInfo
AuthenticationInfo
# 使用MD5、盐salt 和散列
MD5算法
作用:一般用来加密或者签名校验(文件MD5值的比较)
特点:MD5算法不可逆,只能明文生成密文。如果内容相同,无论执行多少次MD5,生成的结果始终一致。 生成结果始终是一个16进制32位长度字符串
相关类: Md5Hash
HashedCredentialsMatcher
ByteSource.Util.bytes("x0~*Y")
// 定义自定义的Realm, 并使用MD5加盐凭证匹配器
CustomizeMd5Realm realm = new CustomizeMd5Realm();
// 设置 Realm 使用MD5算法的哈希凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5"); // 使用MD5算法
credentialsMatcher.setHashIterations(1024); // 散列次数
realm.setCredentialsMatcher(credentialsMatcher);
// md5 + 盐 + 散列
Md5Hash md5Hash = new Md5Hash("123", "x0~*Y", 1024);
// 转成16进制
String s = md5Hash.toHex();
2
3
4
5
6
7
8
9
10
11
12
// 参数1:用户名 参数2:md5+盐后的值 参数3:盐值 参数4:realm的名字
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
principal, "d6206916641be56dc7eb24e04e3463a5",
ByteSource.Util.bytes("x0~*Y"), // 注册时(数据库中存储)的随机盐
this.getName()
);
2
3
4
5
6
# 三、Shiro授权
授权可简单理解为who对what(which)进行How操作:
Who,即主体(Subject),主体需要访问系统中的资源。
What,即资源(Resource),如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括
资源类型
和资源实例
,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例。How,权限/许可(Permission),规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。
权限分为粗颗粒和细颗粒,粗颗粒权限是指对资源类型的权限,细颗粒权限是对资源实例的权限。
主体、资源、权限关系如下图:
# 授权方式
基于角色 RBAC (Role-Based Access Control)
基于权限 RBAC (Resource-Based Access Control)
# 权限字符串
在ini文件中用户、角色、权限的配置规则是:“用户名=密码,角色1,角色2...” “角色=权限1,权限2...”,首先根据用户名找角色,再根据角色找权限,角色是权限集合。
权限字符串的规则是:资源标识符:操作:资源实例标识符
,意思是对哪个资源的哪个实例具有什么操作,**“:”**是资源/操作/实例的分割符,权限字符串也可以使用 * 通配符。
用户创建权限:user:create,或user:create:* 用户修改实例001的权限:user:update:001 用户实例001的所有权限:user:*:001
一般而已,我们操作只需要关注前面两节:
资源:操作 :
: : 所有资源的所有操作权限--->admin
# 授权实现方式
1、 编程式 subject.hasRole("")
2、注解式 @RequiresPermissions("sys:schedule:list")
@RequiresRoles
3、标签式: jsp thymeleaf
/**
* Authorization 授权
* 该方法可能会被调用多次,如果每次都去查数据库,性能较低。应该考虑缓存
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 拿到主体的身份信息(username)
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
System.out.println("---");
// 根据身份信息,从数据库查询其角色信息(权限信息)
// 假设:ysq 是 admin, user
if ("ysq".equals(primaryPrincipal)) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRole("admin");
authorizationInfo.addRole("user");
//authorizationInfo.addStringPermission("user:*:01");
return authorizationInfo;
}
return null;
}
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
# 四、Shiro与Springboot整合
# 整合思路:
资源分类: 公共资源、受限资源
将所有的请求交给Shiro进行处理,即过滤器 ShiroFilter
,通过调用SecurityManager
去进行操作,认证通过就进入系统,认证通过再进行授权
# 整合JSP方式
1、搭建jsp环境
<!-- 引入jsp解析依赖 和C标签 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
2
3
4
5
6
7
8
9
10
11
application.properties
#jsp 支持
spring.mvc.view.suffix=.jsp
spring.mvc.view.prefix=/WEB-INF/jsp/
#spring.mvc.view.prefix=/
2
3
4
如果jsp在webapp目录下,需要在在idea中配置工作目录
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:set var="ctx" value="${pageContext.request.contextPath}"/>
2
3
4
若打开jsp页面,浏览器变成了下载,则将maven重新刷新一下
标签名称 | 标签条件(均是显示标签内容) |
---|---|
shiro:authenticated | 登录之后 |
shiro:notAuthenticated | 不在登录状态时 |
shiro:guest | 用户在没有RememberMe时 |
shiro:user | 用户在RememberMe时 |
<shiro:hasAnyRoles name="abc,123" > | 在有abc或者123角色时 |
<shiro:hasRole name="abc"> | 拥有角色abc |
<shiro:lacksRole name="abc"> | 没有角色abc |
<shiro:hasPermission name="abc"> | 拥有权限资源abc |
<shiro:lacksPermission name="abc"> | 没有abc权限资源 |
shiro:principal | 显示用户身份名称 |
<shiro:principal property="username"/> | 显示用户身份中的属性值 |
# 整合Shiro
// 配置认证资源
Map<String, String> filterMap = new HashMap<>();
filterMap.put("/index.jsp","authc"); //
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
2
3
4
5
6
# shiro常见过滤器
anon:例子/admins/**=anon 没有参数,表示可以匿名使用。
authc:例如/admins/user/**=authc表示需要认证(登录)才能使用,FormAuthenticationFilter是表单认证,没有参数
roles:例子/admins/user/=roles[admin],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。
perms:例子/admins/user/=perms[user:add:*],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/=perms["user:add:,user:modify:"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
rest:例子/admins/user/=rest[user],根据请求的方法,相当于/admins/user/=perms[user:method] ,其中method为post,get,delete等。
port:例子/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString
是你访问的url里的?后面的参数。
authcBasic:例如/admins/user/**=authcBasic没有参数表示httpBasic认证
ssl:例子/admins/user/**=ssl没有参数,表示安全的url请求,协议为https
user:例如/admins/user/**=user没有参数表示必须存在用户, 身份认证通过或通过记住我认证通过的可以访问,当登入操作时不做检查
注:
anon,authcBasic,auchc,user是认证过滤器,
perms,roles,ssl,rest,port是授权过滤器
# 退出登陆
/**
* 退出登录
* 销毁主体的认证记录(信息),下次访问需要重新认证
*
* @return
*/
@RequestMapping("/logout")
public ResponseEntity<String> logout() {
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getPrincipal();
userService.logout(user.getUsername());
subject.logout();
return ResponseEntity.ok().build();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ShiroConfig
/**
* Shiro配置
*
* @author Mark sunlightcs@gmail.com
*/
@Configuration
public class ShiroConfig {
@Bean("securityManager")
public SecurityManager securityManager(OAuth2Realm oAuth2Realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(oAuth2Realm);
securityManager.setRememberMeManager(null);
return securityManager;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//oauth过滤
Map<String, Filter> filters = new HashMap<>();
filters.put("oauth2", new OAuth2Filter());
shiroFilter.setFilters(filters);
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/webjars/**", "anon");
filterMap.put("/druid/**", "anon");
filterMap.put("/app/**", "anon");
filterMap.put("/sys/login", "anon");
filterMap.put("/swagger/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/captcha.jpg", "anon");
filterMap.put("/aaa.txt", "anon");
filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
// Shiro生命周期处理器
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
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
# 自定义Realm: OAuth2Realm
@Component
public class OAuth2Realm extends AuthorizingRealm {
@Autowired
private ShiroService shiroService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof OAuth2Token;
}
/**
* 授权(验证权限时调用)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SysUserEntity user = (SysUserEntity)principals.getPrimaryPrincipal();
Long userId = user.getUserId();
//用户权限列表
Set<String> permsSet = shiroService.getUserPermissions(userId);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(permsSet);
return info;
}
/**
* 认证(登录时调用)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String accessToken = (String) token.getPrincipal();
//根据accessToken,查询用户信息
SysUserTokenEntity tokenEntity = shiroService.queryByToken(accessToken);
//token失效
if(tokenEntity == null || tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()){
throw new IncorrectCredentialsException("token失效,请重新登录");
}
//查询用户信息
SysUserEntity user = shiroService.queryUser(tokenEntity.getUserId());
//账号锁定
if(user.getStatus() == 0){
throw new LockedAccountException("账号已被锁定,请联系管理员");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, getName());
return info;
}
}
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
# 自定义Realm: LoginRealm
public class LoginRealm extends AuthorizingRealm {
/**
* 或者在ShiroConfig中设置
*/
public LoginRealm() {
// 匹配器。需要与密码加密规则一致
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 设置匹配器的加密算法
hashedCredentialsMatcher.setHashAlgorithmName(Md5Hash.ALGORITHM_NAME);
// 设置匹配器的哈希散列次数
hashedCredentialsMatcher.setHashIterations(1024);
// 将对应的匹配器设置到Realm中
this.setCredentialsMatcher(hashedCredentialsMatcher);
}
/**
* 可以往Shiro中注册多种Realm。某种Token对象需要对应的Realm来处理。
* 复写该方法表示该方法支持处理哪一种Token
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取身份信息
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
// 从容器中获取UserService组件
UserService userService = (UserService) SpringContextUtils.getBean("userService");
List<Role> roles = userService.getRolesByUsername(primaryPrincipal);
if (!CollectionUtils.isEmpty(roles)) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
for (Role role : roles) {
// System.out.println("---: " + role.getId() + "----" + role.getRoleName());
authorizationInfo.addRole(role.getRoleName());
// 查询当前角色的权限信息
List<Permission> perms = userService.getPermsByRoleId(role.getId());
if (!CollectionUtils.isEmpty(perms)) {
for (Permission perm : perms) {
authorizationInfo.addStringPermission(perm.getPermissionName());
}
}
}
return authorizationInfo;
}
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 从Token中获取身份信息。这里实际上是username,这里从UsernamePasswordToken的源码可以看出
String principal = (String) token.getPrincipal();
// 从IOC容器中获取UserService组件
UserService userService = (UserService) SpringContextUtils.getBean("userService");
User user = userService.findByUsername(principal);
if (!ObjectUtils.isEmpty(user)) {
// 返回正确的信息(数据库存储的),作为比较的基准
return new SimpleAuthenticationInfo(
user, user.getPassword(),
ByteSource.Util.bytes(user.getSalt()), this.getName()
);
}
return null;
}
}
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
# Controller上的权限注解
@RequiresRoles(value = {"admin","user"}) // 同时具有多个角色
@RequiresPermissions(value = {"user:update:*"}) // 权限字符串验证方式
2
# 工具类:ShiroUtils
/**
* Shiro工具类
*
* @author Mark sunlightcs@gmail.com
*/
public class ShiroUtils {
public static Session getSession() {
return SecurityUtils.getSubject().getSession();
}
public static Subject getSubject() {
return SecurityUtils.getSubject();
}
public static SysUserEntity getUserEntity() {
return (SysUserEntity)SecurityUtils.getSubject().getPrincipal();
}
public static Long getUserId() {
return getUserEntity().getUserId();
}
public static void setSessionAttribute(Object key, Object value) {
getSession().setAttribute(key, value);
}
public static Object getSessionAttribute(Object key) {
return getSession().getAttribute(key);
}
public static boolean isLogin() {
return SecurityUtils.getSubject().getPrincipal() != null;
}
public static String getKaptcha(String key) {
Object kaptcha = getSessionAttribute(key);
if(kaptcha == null){
throw new RRException("验证码已失效");
}
getSession().removeAttribute(key);
return kaptcha.toString();
}
}
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
# SpringContextUtils
/**
* Spring Context 工具类
*
* @author Mark sunlightcs@gmail.com
*/
@Component
public class SpringContextUtils implements ApplicationContextAware {
public static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
SpringContextUtils.applicationContext = applicationContext;
}
public static Object getBean(String name) {
return applicationContext.getBean(name);
}
public static <T> T getBean(String name, Class<T> requiredType) {
return applicationContext.getBean(name, requiredType);
}
public static boolean containsBean(String name) {
return applicationContext.containsBean(name);
}
public static boolean isSingleton(String name) {
return applicationContext.isSingleton(name);
}
public static Class<? extends Object> getType(String name) {
return applicationContext.getType(name);
}
}
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
# Shiro整合thymeleaf模板
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
2
3
4
5
引入命名空间
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="Thymeleaf" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
// 配置ShiroDialect:用于thymeleaf和shiro标签配合使用
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
2
3
4
5
# Shiro优化
# 1、设置ehcache缓存
loginRealm.setCacheManager(ehCacheManager);
loginRealm.setCachingEnabled(true); // 开启全局缓存
loginRealm.setAuthenticationCachingEnabled(true); // 认证缓存
loginRealm.setAuthenticationCacheName("authenticationCache"); // 认证缓存名称
loginRealm.setAuthorizationCachingEnabled(true); // 授权缓存
loginRealm.setAuthorizationCacheName("authorizationCache"); // 授权缓存的名称
2
3
4
5
6
# 2、设置redis缓存
可以参考开源项目: https://github.com/alexxiyang/shiro-redis
思路: 参考 EhcacheManager
需实现 CacheManager
(传入的是缓存名称,返回Cache
的实现, 如 EhCache
)
存在的问题: 盐的序列化问题(解决办法:自定义类实现ByteSource
,参考SimpleByteSource
, 实现序列化接口); RestTemplate
中 key hashValue 中的Key的序列化问题
/**
* Redis配置
*
* @author Mark sunlightcs@gmail.com
*/
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory factory;
@Bean
@ConditionalOnClass(RedisOperations.class)
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(mapper);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用 String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的 key也采用 String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用 jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的 value序列化方式采用 jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForHash();
}
@Bean
public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
return redisTemplate.opsForValue();
}
@Bean
public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForList();
}
@Bean
public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForSet();
}
@Bean
public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForZSet();
}
}
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
# 3、加入验证码
- 前端页面加上验证码, 并后台写上对应的action
- 放行验证码
- 在login的action中去处理验证码判断逻辑
/**
* 生成验证码配置
*
* @author Mark sunlightcs@gmail.com
*/
@Configuration
public class KaptchaConfig {
@Bean
public DefaultKaptcha producer() {
Properties properties = new Properties();
properties.put("kaptcha.border", "no");
properties.put("kaptcha.textproducer.font.color", "black");
properties.put("kaptcha.textproducer.char.space", "5");
properties.put("kaptcha.textproducer.font.names", "Arial,Courier,cmr10,宋体,楷体,微软雅黑");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
写service
/**
* 系统验证码
*
* @author Mark sunlightcs@gmail.com
*/
@Data
@TableName("sys_captcha")
public class SysCaptchaEntity {
@TableId(type = IdType.INPUT)
private String uuid;
/**
* 验证码
*/
private String code;
/**
* 过期时间
*/
private Date expireTime;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service("sysCaptchaService")
public class SysCaptchaServiceImpl extends ServiceImpl<SysCaptchaDao, SysCaptchaEntity> implements SysCaptchaService {
@Autowired
private Producer producer;
@Override
public BufferedImage getCaptcha(String uuid) {
if(StringUtils.isBlank(uuid)){
throw new RRException("uuid不能为空");
}
//生成文字验证码
String code = producer.createText();
SysCaptchaEntity captchaEntity = new SysCaptchaEntity();
captchaEntity.setUuid(uuid);
captchaEntity.setCode(code);
//5分钟后过期
captchaEntity.setExpireTime(DateUtils.addDateMinutes(new Date(), 5));
this.save(captchaEntity);
return producer.createImage(code);
}
@Override
public boolean validate(String uuid, String code) {
SysCaptchaEntity captchaEntity = this.getOne(new QueryWrapper<SysCaptchaEntity>().eq("uuid", uuid));
if(captchaEntity == null){
return false;
}
//删除验证码
this.removeById(uuid);
if(captchaEntity.getCode().equalsIgnoreCase(code) && captchaEntity.getExpireTime().getTime() >= System.currentTimeMillis()){
return true;
}
return false;
}
}
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
写action
/**
* 登录相关
*
* @author Mark sunlightcs@gmail.com
*/
@RestController
public class SysLoginController extends AbstractController {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysUserTokenService sysUserTokenService;
@Autowired
private SysCaptchaService sysCaptchaService;
/**
* 验证码
*/
@GetMapping("captcha.jpg")
public void captcha(HttpServletResponse response, String uuid)throws IOException {
response.setHeader("Cache-Control", "no-store, no-cache");
response.setContentType("image/jpeg");
//获取图片验证码
BufferedImage image = sysCaptchaService.getCaptcha(uuid);
ServletOutputStream out = response.getOutputStream();
ImageIO.write(image, "jpg", out);
IOUtils.closeQuietly(out);
}
/**
* 登录
*/
@PostMapping("/sys/login")
public Map<String, Object> login(@RequestBody SysLoginForm form)throws IOException {
boolean captcha = sysCaptchaService.validate(form.getUuid(), form.getCaptcha());
if(!captcha){
return R.error("验证码不正确");
}
//用户信息
SysUserEntity user = sysUserService.queryByUserName(form.getUsername());
//账号不存在、密码错误
if(user == null || !user.getPassword().equals(new Sha256Hash(form.getPassword(), user.getSalt()).toHex())) {
return R.error("账号或密码不正确");
}
//账号锁定
if(user.getStatus() == 0){
return R.error("账号已被锁定,请联系管理员");
}
//生成token,并保存到数据库
R r = sysUserTokenService.createToken(user.getUserId());
return r;
}
/**
* 退出
*/
@PostMapping("/sys/logout")
public R logout() {
sysUserTokenService.logout(getUserId());
return R.ok();
}
}
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
# 4、session管理
Shiro的认证和授权是基于session实现的,Shiro包含了对session的管理
自定义session管理器
DefaultWebSessionManager
将自定义session管理器设置给SecurityManager
可以在Service等全局地方访问 session里面的数据
5、RememberMe
RememberMeManager rememberMeManager = new CookieRememberMeManager();
6、多Realm配置
将多个realm配置进
ModularRealmAuthenticator
, 再配置securityManager.setAuthenticator(authenticator);
Collection<Realm> realms = Lists.newArrayList(); securityManager.setRealms(realms); ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator(); authenticator.setRealms(realms); securityManager.setAuthenticator(authenticator);
1
2
3
4
5配置认证策略
# 五、权限数据库表设计
# 六、相关项目
开涛学Shiro示例: https://github.com/zhangkaitao/shiro-example