shiro实践

实践这几张图

能实现的功能有哪些:

ShiroFeatures

具体的架构图:

ShiroBasicArchitecture

ShiroArchitecture

官网找例子

https://shiro.apache.org/

Integrations - spring

页面中有连接是连接到github。

https://github.com/apache/shiro/tree/main/samples

这是官方提供的例子。

main方法使用

参考 https://github.com/apache/shiro/blob/main/samples/quickstart

创建maven项目:

加入依赖:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.9.0</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

创建shiro.ini

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
# =============================================================================
# Quickstart INI Realm configuration
#
# For those that might not understand the references in this file, the
# definitions are all based on the classic Mel Brooks' film "Spaceballs". ;)
# =============================================================================

# -----------------------------------------------------------------------------
# Users and their assigned roles
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setUserDefinitions JavaDoc
# -----------------------------------------------------------------------------
[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz

# -----------------------------------------------------------------------------
# Roles with assigned permissions
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5
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
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Simple Quickstart application showing how to use Shiro's API.
*
* @since 0.9 RC2
*/
public class Quickstart {

private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);


public static void main(String[] args) {

// The easiest way to create a Shiro SecurityManager with configured
// realms, users, roles and permissions is to use the simple INI config.
// We'll do that by using a factory that can ingest a .ini file and
// return a SecurityManager instance:

// Use the shiro.ini file at the root of the classpath
// (file: and url: prefixes load from files and urls respectively):
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();

// for this simple example quickstart, make the SecurityManager
// accessible as a JVM singleton. Most applications wouldn't do this
// and instead rely on their container configuration or web.xml for
// webapps. That is outside the scope of this simple quickstart, so
// we'll just do the bare minimum so you can continue to get a feel
// for things.
SecurityUtils.setSecurityManager(securityManager);

// Now that a simple Shiro environment is set up, let's see what you can do:

// get the currently executing user:
Subject currentUser = SecurityUtils.getSubject();

// Do some stuff with a Session (no need for a web or EJB container!!!)
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("检测到正确的值! [" + value + "]");
}

// let's login the current user so we can check against roles and permissions:
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
try {
currentUser.login(token);
} catch (UnknownAccountException uae) {
log.info("没有用户名为 " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("密码对于账户 " + token.getPrincipal() + " 不正确!");
} catch (LockedAccountException lae) {
log.info("账户 " + token.getPrincipal() + " 被锁定 " +
"请联系管理员解锁.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}

//say who they are:
//print their identifying principal (in this case, a username):
log.info("User [" + currentUser.getPrincipal() + "] 登录成功.");

//test a role:
if (currentUser.hasRole("schwartz")) {
log.info("拥有schwartz角色!");
} else {
log.info("没有schwartz角色.");
}

//test a typed permission (not instance-level)
if (currentUser.isPermitted("lightsaber:wield")) {
log.info("拥有lightsaber:wield权限 .");
} else {
log.info("没有lightsaber:wield权限");
}

//a (very powerful) Instance Level permission:
if (currentUser.isPermitted("winnebago:drive:eagle5")) {
log.info("拥有winnebago:drive:eagle5权限");
} else {
log.info("没有winnebago:drive:eagle5权限");
}

//all done - log out!
currentUser.logout();

System.exit(0);
}
}

currentUser.login(token); 是 Authenticator, 完成认证

currentUser.isPermitted(“winnebago:drive:eagle5”) 是Authorizer, 是查看是否有权限

spring boot 使用shiro(无页面)

加入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.9.0</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>

domain类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 用户
*/
@Data
public class User implements Serializable {

private int id;

private String username;

private String password;

private Date createTime;

private String salt;


/**
* 角色集合
*/
private List<Role> roleList;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 角色
*/
@Data
public class Role implements Serializable {

private int id;

private String name;

private String description;

private List<Permission> permissionList;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 权限
*/
@Data
public class Permission implements Serializable{

private int id;

private String name;

private String url;

}
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
@Slf4j
@Service
public class UserService {

public User findAllUserInfoByUsername(String username) {

//业务方法里面加缓存,也可以

User user = new User();
user.setUsername("test1");
user.setPassword("123456");

//用户的角色集合
List<Role> roleList = new ArrayList<>();
Role role1 = new Role();
role1.setName("role1");
List<Permission> permissionList = new ArrayList<>();
Permission permission1 = new Permission();
permission1.setName("permission1");
permission1.setUrl("/add");
permissionList.add(permission1);
role1.setPermissionList(permissionList);
roleList.add(role1);

user.setRoleList(roleList);

return user;
}

public User findSimpleUserInfoByUsername(String username) {
User user = new User();
user.setUsername("test1");
user.setPassword("123456");
return user;
}

}

因为自带的realm不好用,所以自定义realm,realm又是一个Authorizer(不能自动创建Authorizer),所以需要手动创建SecurityManager

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
/**
* 自定义realm
*/
public class CustomRealm extends AuthorizingRealm {

@Autowired
private UserService userService;

/**
* 进行权限校验的时候回调用
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("授权 doGetAuthorizationInfo");

User newUser = (User)principals.getPrimaryPrincipal();
User user = userService.findAllUserInfoByUsername(newUser.getUsername());

List<String> stringRoleList = new ArrayList<>();
List<String> stringPermissionList = new ArrayList<>();


List<Role> roleList = user.getRoleList();

for(Role role : roleList){
stringRoleList.add(role.getName());

List<Permission> permissionList = role.getPermissionList();

for(Permission p: permissionList){
if(p!=null){
stringPermissionList.add(p.getName());
}
}

}

SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRoles(stringRoleList);
simpleAuthorizationInfo.addStringPermissions(stringPermissionList);

return simpleAuthorizationInfo;
}

/**
* 用户登录的时候会调用
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

System.out.println("认证 doGetAuthenticationInfo");

//从token获取用户信息,token代表用户输入
String username = (String)token.getPrincipal();

User user = userService.findAllUserInfoByUsername(username);

//取密码
String pwd = user.getPassword();
if(pwd == null || "".equals(pwd)){
return null;
}

return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName());
}
}
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

@Configuration
public class ShiroConfig {

@Bean
public CustomRealm customRealm() {
return new CustomRealm();
}

@Bean
public DefaultWebSecurityManager getSecurityManager(CustomRealm customRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm);
return securityManager;
}


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

System.out.println("执行 ShiroFilterFactoryBean.shiroFilter()");

ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

//必须设置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);


//需要登录的接口,如果访问某个接口,需要登录却没登录,则调用此接口(如果不是前后端分离,则跳转页面)
shiroFilterFactoryBean.setLoginUrl("/pub/need_login");

//登录成功,跳转url,如果前后端分离,则没这个调用
shiroFilterFactoryBean.setSuccessUrl("/");

//没有权限,未授权就会调用此方法, 先验证登录-》再验证是否有权限
shiroFilterFactoryBean.setUnauthorizedUrl("/pub/not_permit");


//拦截器路径,坑一,部分路径无法进行拦截,时有时无;因为同学使用的是hashmap, 无序的,应该改为LinkedHashMap
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

//退出过滤器
filterChainDefinitionMap.put("/logout","logout");

//匿名可以访问,也是就游客模式
filterChainDefinitionMap.put("/pub/**","anon");

//登录用户才可以访问
filterChainDefinitionMap.put("/authc/**","authc");

//坑二: 过滤链是顺序执行,从上而下,一般讲/** 放到最下面

//authc : url定义必须通过认证才可以访问
//anon : url可以匿名访问
filterChainDefinitionMap.put("/**", "authc");

shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

return shiroFilterFactoryBean;
}

}

创建测试controller

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


/**
* 登录接口
* @param user
* @param request
* @param response
* @return
*/
@PostMapping("login")
public ResponseEntity login(@RequestBody User user, HttpServletRequest request, HttpServletResponse response){

Subject subject = SecurityUtils.getSubject();
Map<String,Object> info = new HashMap<>();
try {
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(user.getUsername(), user.getPassword());

subject.login(usernamePasswordToken);

info.put("msg","登录成功");
info.put("session_id", subject.getSession().getId());

return ResponseEntity.ok(info);

} catch (Exception e){
log.info("登录失败", e);
info.put("msg","账号或者密码错误");
return ResponseEntity.ok("账号或者密码错误");
}

}

@RequestMapping("need_login")
public ResponseEntity needLogin(){
Map<String,Object> info = new HashMap<>();
info.put("msg","请登录");
return ResponseEntity.ok(info);
}


@RequestMapping("not_permit")
public ResponseEntity notPermit(){
Map<String,Object> info = new HashMap<>();
info.put("msg","没权限");
return ResponseEntity.ok(info);
}

}
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

@RestController
@RequestMapping("private")
public class PrivateController {


@RequestMapping("list")
@RequiresRoles("role1")
public ResponseEntity list(){

List<String> list = new ArrayList<>();
list.add("list1");
list.add("list2");
list.add("list3");

return ResponseEntity.ok(list);

}

@RequestMapping("list2")
@RequiresPermissions("permission2")
public ResponseEntity list2(){

List<String> list = new ArrayList<>();
list.add("list1");
list.add("list2");
list.add("list3");

return ResponseEntity.ok(list);

}

}

这时直接访问http://localhost:8080/private/list会跳转到登录接口返回 请登录

http://localhost:8080/pub/login

1
2
3
4
{
"username": "test1",
"password": "123456"
}

登录 再访问,可以正常返回。

到这里已经实现了 Authentication、 Authorization

现在使用的密码还是明文密码要改成密文。使用的SimpleCredentialsMatcher。

默认 Session Management 是 new DefaultWebSecurityManager() 中的new ServletContainerSessionManager() 这个是使用的tomcat的session。

new CustomRealm() 时调用父类时,CacheManager cacheManager传的是空,没有启用。

Credentials Matcher 凭据匹配器

在shrioConfig中添加

1
2
3
4
5
6
7
8
9
10
11
12
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();

//散列算法,使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");

//散列的次数,比如散列两次,相当于 md5(md5("xxx"));
hashedCredentialsMatcher.setHashIterations(2);

return hashedCredentialsMatcher;
}

修改实例realm的bean

1
2
3
4
5
6
@Bean
public CustomRealm customRealm() {
CustomRealm customRealm = new CustomRealm();
customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return customRealm;
}

这时再使用123456登录会提示账号或密码错误

把userService中的密码改为4280d89a5a03f812751f504cc10ee8a5 就可以登录成功。

redis单机版 CacheManager

加入依赖

1
2
3
4
5
6
7
8
9
10
11
12
<!-- shiro+redis缓存插件 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
</exclusions>
</dependency>

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
/**
* 配置redisManager
*
*/
public RedisManager getRedisManager(){
RedisManager redisManager = new RedisManager();
redisManager.setHost("localhost");
redisManager.setPort(6379);
redisManager.setPassword("123456");
return redisManager;
}

/**
* 配置具体cache实现类
* @return
*/
public RedisCacheManager cacheManager(){
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(getRedisManager());
//设置过期时间,单位是秒,20s,
redisCacheManager.setExpire(20);

return redisCacheManager;
}

修改SecurityManager

1
2
3
4
5
6
7
@Bean
public DefaultWebSecurityManager getSecurityManager(CustomRealm customRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm);
securityManager.setCacheManager(cacheManager());
return securityManager;
}

测试doGetAuthorizationInfo 接口就不在走了。

Redis单机版 SessionManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 自定义session持久化
* @return
*/
public RedisSessionDAO redisSessionDAO(){
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(getRedisManager());
return redisSessionDAO;
}


@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
defaultWebSessionManager.setSessionDAO(redisSessionDAO());
return defaultWebSessionManager;
}

1
2
3
4
5
6
7
8
@Bean
public DefaultWebSecurityManager getSecurityManager(CustomRealm customRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setSessionManager(sessionManager());
securityManager.setCacheManager(cacheManager());
securityManager.setRealm(customRealm);
return securityManager;
}

这时登录后,去redis里查看就可以看到shiro:session的key,说明使用redis做session管理成功。

RememberMe 记住我

其实就是存的更久一点。设置cookies的过期时间长一点。如果有这个cookies就直接放行,表示登录成功。

1
2
3
4
5
6
7
8
1、 Cookie 写到客户端并 保存
2、 通过调用subject.login()前,设置 token.setRememberMe(true);
3、 关闭浏览器再重新打开;会发现浏览器还是记住你的
4、 注意点:
- subject.isAuthenticated() 表示用户进行了身份验证登录的,即Subject.login 进行了登录
- subject.isRemembered() 表示用户是通过RememberMe登录的
- subject.isAuthenticated()==true,则 subject.isRemembered()==false, 两个互斥
- 总结:特殊页面或者API调用才需要authc进行验证拦截,该拦截器会判断用户是否是通过 subject.login()登录,安全性更高,其他非核心接口或者页面则通过user拦截器处理即可

退出

方法一:

在shiro的filter中添加:

1
filterChainDefinitionMap.put("/logout","logout");

方法二:

添加退出接口

1
2
3
4
5
6
7
8
9
10

@RequestMapping("/logout")
public ResponseEntity findMyPlayRecord(){

Subject subject = SecurityUtils.getSubject();

SecurityUtils.getSubject().logout();

return ResponseEntity.ok("logout成功");
}

shiro实践
http://hanqichuan.com/2022/05/18/权限框架/shiro实践/
作者
韩启川
发布于
2022年5月18日
许可协议