Springboot-shiro

Shiro简介

其实这个就是一个关于做安全管理的框架,他不仅可以和javaEE结合也可以和javaSE结合

提供的功能

  • Authentication:身份认证,登录,验证用户的身份
  • Authorization:授权,
  • Session Management: Shiro内置的session,对其进行管理
  • Cryptography:加密,保证数据的安全性
  • Web Support: web支持,可以很好的集成到web环境
  • Caching: 缓存,
  • Concurrency: 多并发
  • Testing:测试
  • Remember Me:”记住我”的功能

Shiro结构

我们来观察一下Shiro的结构

解释一下出现的名词

  • subject:与当前应用交互的任何东西都可以是Subject,与Subject的所有交互都会委托给SecurityManager,Subject其实只是一个门面,SecurityManager 才是实际的执行者
  • SecurityManager:安全管理器,即所有与安全有关的操作都会与SecurityManager交互,并且它管理着所有的Subject,它是Shiro的核心,它负责与Shiro的其他组件进行交互
  • Realm: Shiro从Realm获取安全数据(如用户,角色,权限),就是说SecurityManager 要验证用户身份,那么它需要从Realm 获取相应的用户进行比较,来确定用户的身份是否合法;也需要从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
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.8.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/jcl-over-slf4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.24</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.24</version>
</dependency>
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

配置文件

log4j.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
log4j.rootLogger=INF0,stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n

# GeneraL Apache Libraries
log4j.logger.org.apache=WARN

# Spring
log4j.logger.org.springframework=WARN

# Defautt Shiro Logging
log4j.logger.org.apache.shiro=INF0

# DisabLe verbose Logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN

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
[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 rote 'president'
presidentskroob = 12345, president
# user 'darkhe met' with password 'Ludicrousspeed’and rotes 'darklord’and 'schwartz
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'tonestarr' 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.reatm.text.TextConfigurationReatm#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
# ticense plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5

ShiroQuickStart.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
public class ShiroQuickStart {
private static final Logger LOG = LoggerFactory.getLogger(ShiroQuickStart.class);
public static void main(String[] args) {
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
session.setAttribute("someKey","aValue");
String value = (String) session.getAttribute("someKey");
if("aValue".equals(value)){
LOG.info("Retrieved the correct value");
}
if(!currentUser.isAuthenticated()){
UsernamePasswordToken token = new UsernamePasswordToken("lonestar", "vespa");
token.setRememberMe(true);
try{
currentUser.login(token);
}catch (UnknownAccountException uae){
LOG.info("There is no user with username whith "+ token.getPrincipal());
}catch (IncorrectCredentialsException ice){
LOG.info("Password for account"+ token.getPrincipal()+"was incorrect");
}catch (LockedAccountException lae){
LOG.info("The Account for userName"+ token.getPrincipal()+"is locked. " +
"Please contact your administrator to unlocked it");
}catch (AuthenticationException ae){
//unexpected exception
}
}

LOG.info("USER["+ currentUser.getPrincipal()+"] logged in successfully");
if(currentUser.hasRole("schwartz")){
LOG.info("May the Schwartz be with you");
}else {
LOG.info("Hello,Mere mortal");
}

if(currentUser.isPermitted("lightsaber:wield")){
LOG.info("You may use a lightsaber ring use it wisely");
}else {
LOG.info("Sorry, light rings are for schwartz masters only");
}

if(currentUser.isPermitted("winnebago:drive:eagle5")){
LOG.info("You are permitted to 'drive' the winnebago with license plate(id) 'eagle5'. " +
"Here are the keys-have fun!");
}else {
LOG.info("Sorry,you aren't allowed to drive the 'eagle5' winnebago");
}
currentUser.logout();
System.exit(0);
}
}

运行输出

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
2022-02-17 14:33:37,982 DEBUG [org.apache.shiro.io.ResourceUtils] - 
Opening resource from class path [shiro.ini]

2022-02-17 14:33:38,021 DEBUG [org.apache.shiro.config.Ini] - Parsing [users]

2022-02-17 14:33:38,026 DEBUG [org.apache.shiro.config.Ini] - Parsing [roles]

2022-02-17 14:33:38,523 DEBUG [org.apache.shiro.config.IniFactorySupport] -
Creating instance from Ini [sections=users,roles]

2022-02-17 14:33:38,571 DEBUG [org.apache.shiro.realm.text.IniRealm] -
Discovered the [roles] section. Processing...

2022-02-17 14:33:38,575 DEBUG [org.apache.shiro.realm.text.IniRealm] -
Discovered the [users] section. Processing...

2022-02-17 14:33:38,594 DEBUG [org.apache.shiro.session.mgt.AbstractValidatingSessionManager] -
No sessionValidationScheduler set. Attempting to create default instance.

2022-02-17 14:33:38,595 INFO [org.apache.shiro.session.mgt.AbstractValidatingSessionManager] -
Enabling session validation scheduler...

2022-02-17 14:33:38,625 DEBUG [org.apache.shiro.session.mgt.DefaultSessionManager] -
Creating new EIS record for new session instance [org.apache.shiro.session.mgt.SimpleSession,id=null]

2022-02-17 14:33:39,022 INFO [com.lizhi.springbootshiro.start.ShiroQuickStart] - Retrieved the correct value

2022-02-17 14:33:39,022 DEBUG [org.apache.shiro.realm.AuthenticatingRealm] -
Looked up AuthenticationInfo [null] from doGetAuthenticationInfo

2022-02-17 14:33:39,022 DEBUG [org.apache.shiro.realm.AuthenticatingRealm] - No AuthenticationInfo found for submitted AuthenticationToken [org.apache.shiro.authc.UsernamePasswordToken - lonestar, rememberMe=true]. Returning null.

2022-02-17 14:33:39,030 INFO [com.lizhi.springbootshiro.start.ShiroQuickStart] -
There is no user with username whith lonestar

SpringBoot整合Shiro

先按照以下步骤把基本的环境搭好

  1. 导入依赖,这里只显示shiro的相关依赖
1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.8.0</version>
</dependency>
  1. 实体类User.java
1
2
3
4
5
6
7
8
9
10
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User {
private int id;
private String userName;
private String password;
private String perms;
}
  1. 配置类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
@Configuration
public class ShiroConfig {
//ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactory(@Autowired DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
return shiroFilterFactoryBean;
}

//DefaultWebSecurityManager
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Autowired UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
return securityManager;
}

//realm,需要自定义,用来做授权和认证的
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
  1. UserRealm.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("授权");
return null;
}

//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("认证");
return null;
}
}
  1. index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h2>首页</h2>
<ul>
<a th:href="@{/user/add}">用户增</a>
<a th:href="@{/user/del}">用户删</a>
<a th:href="@{/user/update}">用户改</a>
</ul>
</body>
</html>
  1. user的add页面
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>user的add</h3>
</body>
</html>
  1. user的del页面
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>user的delete</h3>
</body>
</html>
  1. user的update页面
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>user的update</h3>
</body>
</html>
  1. MyController.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
package com.lizhi.springbootshiro.contoller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyController {
@GetMapping("/index")
public String index(Model model){
model.addAttribute("msg","hello,shiro");
return "index";
}

//这里是restful风格
@GetMapping("/user/{method}")
public String operateUser(@PathVariable("method") String method){
return "user/"+method;
}
}
  1. UserMapper.java

user表中就三个字段。id,user_name,password,perms

1
2
3
4
5
@Mapper
public interface UserMapper {
List<User> listAllUsers();
User getUserByName(String userName);
}
  1. UserMapper.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
<?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.lizhi.springbootshiro.mapper.UserMapper">

<select id="listAllUsers" resultType="com.lizhi.springbootshiro.pojo.User">
select id,user_name as userName,password,perms from `user`
</select>
<select id="getUserByName" resultType="com.lizhi.springbootshiro.pojo.User">
select id,user_name as userName,password,perms from `user` where user_name = #{userName}
</select>

</mapper>

登录拦截

登录拦截指的是,未登录的用户不得进入用户的增删改页面

下面是集中拦截规则,一般authc和perms用的比较多

1
2
3
4
5
6
7
/**
* anno: 无需认证即可访问
* authc:必须认证了才可以访问
* perms:必须拥有权限才可以访问
* roles:必须拥有某种角色才可以访问
* user:必须拥有记住我的功能的时候才可以访问(一般很少用)
*/

这是在ShiroFilterFactoryBean中配置的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Bean
public ShiroFilterFactoryBean getShiroFilterFactory(@Autowired DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

Map<String,String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/add","perms[user:add]"); //设置该路径只有user:add才可以访问
filterMap.put("/user/update","perms[user:update]"); //设置该路径只有user:update才可以访问
filterMap.put("/user/del","perms[user:del]"); //设置该路径只有user:del才可以访问
filterMap.put("/user/*","authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);

shiroFilterFactoryBean.setLoginUrl("/login"); //设置登录的路径
shiroFilterFactoryBean.setSuccessUrl("/index"); //设置登录成功的路径
shiroFilterFactoryBean.setUnauthorizedUrl("/unauth"); //设置未授权的用户被访问后跳转的路径
return shiroFilterFactoryBean;
}

login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<h2>登录</h2>
<p style="color: red">[[${error}]]</p>
<form th:action="@{/login.do}">
<p>
<input type="text" name="userName" placeholder="请输入您的用户名">
</p>
<p>
<input type="password" name="password" placeholder="请输入您的密码">
</p>
<p>
<input type="submit" value="登录">
</p>
</form>
</body>
</html>

MyController中配置路径

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
@GetMapping("/login")
public String login(){
return "login";
}

@GetMapping("/unauth")
@ResponseBody //这里偷懒没写页面了,而是直接将消息返回到页面中
public String unauthorized(){
return "未经授权无法访问此页面";
}
@GetMapping("/login.do")
public String doLogin(String userName, String password, Model model) {
//获取当前用户
Subject currentUser = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
try{
currentUser.login(token);//这里下一步是到userRealm的认证方法
//如果没有抛出异常,则说明登录成功
Subject currentUser = SecurityUtils.getSubject();
//拿到shiro内部的session保存数据,
currentUser.getSession().setAttribute("loginInfo",user);
return "index";
}catch (UnknownAccountException uae){
model.addAttribute("error","用户名错误");
return "login";
}catch (IncorrectCredentialsException ice){
model.addAttribute("error","密码错误");
return "login";
}
}

用户认证

案例走到这里,你会发现无论怎么登录都是错的,那是因为我们还没有对用户进行认证。

用户认证指的是,当用户登录的时候,对其进行验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("认证");
/**
* 数据库中读取数据
*/
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
User user = userMapper.getUserByName(token.getUsername());
if(user==null){
//这里如果返回null的话,会自动抛出异常到下图处
return null;
}
//这里的第一个参数user会设置给当前登录对象的Principal中。
//可以通过SecurityUtils.getSubject().getPrincipal()获取
//第二个参数传入正确的密码,即数据库中的密码,交给shiro来做密码的校验,如果密码错误,也会抛出异常到下图的地方
return new SimpleAuthenticationInfo(user, user.getPassword(),"");
}

这里插上一嘴,在web环境中,通过subject拿到的session即web框架中的那个session。

但是如果shiro没有使用在web环境的话,内部也是有一个session的,但是那个就不是web中的session了。

用户授权

好啦,现在案例已经可以跑起来了,并且如果账号密码正确的话是可以登录的,并且返回到主页

还记得我们在下图配置的这一串让人一头雾水的配置吗?

这里就只是配置了一个规则,但是具体登录的用户的授权还没有做,我们现在去完成它吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("授权");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//这里需要根据用户表的数据进行增加权限
Subject subject = SecurityUtils.getSubject();
//拿到用户认证的时候传入的user
User currentUser = (User) subject.getPrincipal();
System.out.println("权限为:"+ currentUser.getPerms());
//数据库中perms字段是以这样的形式存储的 user:add,user:update
//所以需要将其用,分割然后使用工具类将其转换为集合传入addStringPermissions
info.addStringPermissions(Arrays.asList(currentUser.getPerms().split(",")));
return info;
}

现在就完成了所有的配置啦!大家可以尝试一下

这里再多完成一个需求,就是首页只显示用户具有权限的链接

首先需要导入thymeleaf的依赖,然后再导入命名空间xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"

1
2
3
4
5
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.1.0</version>
</dependency>

配置一个bean

1
2
3
4
5
//整合thymeleaf
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}

index.html

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
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>首页</h2>
<p>[[${msg}]]</p>
<!--当session中没有登录信息的时候显示-->
<p th:if="${session.loginInfo==null}"><a th:href="@{/login}">登录</a></p>
<!--当session有信息的时候显示-->
<p th:if="${session.loginInfo!=null}"><a th:href="@{/logout}">注销</a></p>

<!--或者像这样写,不用将用户信息存入session
<p shiro:notAuthenticated><a th:href="@{/login}">登录</a></p>
<p shiro:authenticated><a th:href="@{/logout}">注销</a></p>
-->
<ul>
<li shiro:hasPermission="user:add"><a th:href="@{/user/add}">用户增</a></li>
<li shiro:hasPermission="user:del"><a th:href="@{/user/del}">用户删</a></li>
<li shiro:hasPermission="user:update"><a th:href="@{/user/update}">用户改</a></li>
</ul>
</body>
</html>

注销

大家都应该注意到了,上面的页面中多了一个注销的链接,接下来我们来完成一下注销的功能

1
2
3
4
5
@GetMapping("/logout")
public String logout(){
SecurityUtils.getSubject().logout();
return "index";
}

是的,你没有看错,就是这么简单,shiro已经将其完美得封装好了

以上

给作者买杯咖啡吧~~~