0%

【SpringBoot】Shiro安全框架

Shiro安全框架的简单介绍,以及与SpringBoot的整合


一、Shiro快速开始

1.1 Shiro介绍

  • Apache Shiro是一个Java的安全框架
  • Shiro可以非常容易开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境中
  • Shiro可以完成,认证,授权,加密,会话管理,Web集成,缓存等

1.2 快速入门

  • 下载后,按照官方文档,将sample/quickstart下的maven项目复制到IDEA中,进行测试
  • 由于Maven文件中继承了父maven,依赖没有版本号,所以要手动导入依赖
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
<!--shiro安全框架-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.7.0</version>
</dependency>

<!-- configure logging 配置日志-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<scope>runtime</scope>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<scope>runtime</scope>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<scope>runtime</scope>
<version>1.2.17</version>
</dependency>
  • 运行Quickstart,显示以下结果,证明运行成功

二、Shiro分析

  • 源码分析
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
public class Quickstart {

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

public static void main(String[] args) {

// Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
// SecurityManager securityManager = factory.getInstance();
//上面方法过时,新版方法如下
IniRealm iniRealm = new IniRealm("classpath:shiro.ini"); //根据ini文件创建一个IniRealm对象
DefaultSecurityManager securityManager = new DefaultSecurityManager(); //创建安全管理器
securityManager.setRealm(iniRealm); //将IniRealm注入到SecurityManager
SecurityUtils.setSecurityManager(securityManager); //将SecerityManager注入到安全工具类SecurityUtils

//获取当前用户对象Subject
Subject currentUser = SecurityUtils.getSubject();

//通过当前用户获取Session
Session session = currentUser.getSession();
//告诉如何使用Session存值,取值
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("Retrieved the correct value! [" + value + "]");
}

//SpringSecurity一样,判断当前用户是否已认证
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");//生成令牌,用户、密码作为参数
token.setRememberMe(true); //未登陆就生产一个令牌,并且记住此令牌
try {
currentUser.login(token); //执行登陆操作 从ini文件中读取用户令牌信息 lonestarr = vespa, goodguy, schwartz 用户名 = 密码, 角色(权限)...
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + 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 unlock it.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) { //意外未知异常
//unexpected condition? error?
}
}

log.info("User [" + currentUser.getPrincipal() + "] logged in successfully."); //currentUser.getPrincipal():获取用户名

if (currentUser.hasRole("schwartz")) { //currentUser.hasRole("schwartz"):判断拥有的角色
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}

if (currentUser.isPermitted("lightsaber:wield")) { //currentUser.isPermitted("lightsaber:wield"):判断拥有的权限
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber 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);
}
}

三、SpringBoot整合Shiro

创建SpringBoot项目

3.1 导入相关依赖

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

<!--thymeleaf模板引擎-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<!--shiro安全框架-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.7.0</version>
</dependency>

3.2 创建自定义Realm

  • Realm用于设置用户认证和授权
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//自定义Realm
public class UserRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("执行 ==> 授权doGetAuthorizationInfo");
return null;
}

//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行 ==> 认证doGetAuthenticationInfo");
return null;
}
}

3.3 创建Shiro配置类

  • 注入Realm类
  • 注入DefaultWebSecurityManager类,Realm类作为参数传递
  • 注入ShiroFilterFactoryBean类,DefaultWebSecurityManage类作为参数传递
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
@Configuration
public class ShiroConfig {

//创建Realm对象
@Bean //交给Spring来托管
public UserRealm getRealm(){
return new UserRealm();
}

//DefaultWebSecurityManager(对应Shiro的SecurityManager)
@Bean
public DefaultWebSecurityManager getSecurityManager(UserRealm realm){
return new DefaultWebSecurityManager(realm);
}

//ShiroFilterFactoryBean(对应Shiro的subject)
@Bean(name = "shiroFilterFactoryBean") //要起别名为"shiroFilterFactoryBean",否则报错
public ShiroFilterFactoryBean getFilterFactoryBean(DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
filterFactoryBean.setSecurityManager(securityManager);
//---------------------里面配置相关设置---------------------------


//------------------------------------------------------------------------
return filterFactoryBean;
}
}

3.4 简单的环境配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>

<!--添加跳转-->
<a th:href="@{/user/toAdd}">增加用户</a>
<a th:href="@{/user/toUpdate}">修改用户</a>
<a th:href="@{/user/toDelete}">删除用户</a>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
add.html 和 update.html 和 delete.html都类似,改一下h1内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>增加用户</h1>
</body>
</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
Controller接口层
@Controller
public class RouteController {
//对应的跳转
@RequestMapping({"/", "index.thml"})
public String toIndex(){
return "index";
}

@RequestMapping("/user/toAdd")
public String toAdd(){
return "/user/add";
}

@RequestMapping("/user/toUpdate")
public String toUpdate(){
return "/user/update";
}

@RequestMapping("/user/toDelete")
public String toDelete(){
return "/user/delete";
}
}
  • 以上是最基本的Shrio框架的搭建,举例功能下面实现

四、Shiro实现拦截登录

4.1 设置要拦截的请求

  • “/user/toAdd”:无需认证,可以直接访问
  • “/user/toUpdate”:需要认证,才能访问
  • “/user/toDelete”:用户需要”admin”权限才能访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Bean(name = "shiroFilterFactoryBean") //要起别名为"shiroFilterFactoryBean",否则报错
public ShiroFilterFactoryBean getFilterFactoryBean(DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
filterFactoryBean.setSecurityManager(securityManager);
//---------------------里面配置相关设置---------------------------
//添加shiro内容过滤器
/*
anon:无需认证就可以访问
authc:必须认证了才能访问
user:必须拥有“Remenber me”功能才能访问
perms:拥有对某个资源的权限才能访问
role:拥有某个角色权限才能访问
*/
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/toAdd", "anon");
filterMap.put("/user/toUpdate", "authc"); //支持通配符/user/*
filterMap.put("/user/toDelete", "perms[admin]");
filterFactoryBean.setFilterChainDefinitionMap(filterMap); //将上面Map设置到过滤器中
//------------------------------------------------------------------------
return filterFactoryBean;
}

4.2 设置拦截跳转

  • Shrio成功拦截请求,但没有进行登录页面跳转,不想SpringSecurity自带登录页面,需要手动设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>登录</h1>
<form action="">
<p>
用户名:
<input type="text" name="username">
</p>
<p>
密码:
<input type="text" name="password">
</p>
<p>
<input type="submit">
</p>
</form>
</body>
</html>
1
2
3
4
5
Controller跳转Login页面
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Shiro配置类设置登录页面
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean getFilterFactoryBean(DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
filterFactoryBean.setSecurityManager(securityManager);
//---------------------里面配置相关设置---------------------------
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/toAdd", "anon");
filterMap.put("/user/toUpdate", "authc");
filterMap.put("/user/toDelete", "perms[admin]");
filterFactoryBean.setFilterChainDefinitionMap(filterMap);

//需要认证,但是没有认证,会进入登录页面(需要手动配置,SpringSecurity有自带登录页面)
filterFactoryBean.setLoginUrl("/toLogin");
//------------------------------------------------------------------------
return filterFactoryBean;
}

五、Shiro实现用户认证

5.1 Controller层登录业务

  • 在login.html 添加aciton=”/login”进行Controller业务跳转
  • Controller接受登录页面发送的账号、密码,执行登录操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RequestMapping("/login")
public String login(String username, String password, Model model){ //从登录表单中接受数据
//获取当前用户
Subject subject = SecurityUtils.getSubject();
//封装用户登录的数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//进行登录,并且进行异常捕获
try {
subject.login(token);
} catch (UnknownAccountException e) {
model.addAttribute("msg", "用户名错误");
return "login"; //出错返回登录页面
} catch (IncorrectCredentialsException e){
model.addAttribute("msg", "密码错误");
return "login";
}
return "index"; //登录成功返回首页
}

5.2 Realm类实现认证判断

  • subject.login()方法会去执行Realm中的认证方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
     //认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行 ==> 认证doGetAuthenticationInfo");
//登录会执行认证方法
//设置登录需要的用户名 密码
//(1)内存中设置
String name = "莱特雷";
String password = "123";
UsernamePasswordToken userToken = (UsernamePasswordToken)token;
//用户认证
if (!name.equals(userToken.getUsername())){
return null; //会抛出异常 UnknownAccountException(用户名错误)
}
//密码认证,Shiro会自己判断

return new SimpleAuthenticationInfo("",password,"");//参数一:用户对象(没有就不设置),参数二:密码,参数三:密码加密方式
}

六、Shiro整合Mybatis

Shiro整合Mybatis,实现从数据库获取用户、密码,实现数据库认证

6.1 导入相关依赖

1
2
3
4
5
6
7
8
9
10
11
12
<!--MySQL数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<!--SpringBoot Mybatis整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>

6.2 配置文件

1
2
3
4
5
6
7
8
9
10
11
#连接数据库信息
spring:
datasource:
username: root
password: 123
url: jdbc:mysql://localhost:3306/jdbctest?serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver

#Mybatis基础配置
mybatis:
mapper-locations: classpath:mapper/*.xml #Mybatis mapper文件路径

6.3 创建相应的Bean,Mapper,Service

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
Bean
public class Person {
private String name;
private Integer age;
private String password;
private String perms;

public Person() {
}

public Person(String name, Integer age, String password) {
this.name = name;
this.age = age;
this.password = password;
}

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", password='" + password + '\'' +
'}';
}
}
1
2
3
4
5
6
Mapper接口
@Mapper //注意要添加Mapper注解,否则要创建配置类,开启组件扫描
@Repository
public interface PersonMapper {
public Person queryPersonByName(String name); //通过用户名查询该人的信息
}
1
2
3
4
5
6
7
8
9
10
11
12
13
Mapper.xml中SQL
<?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.letere.bean.PersonMapper">
<select id="queryPersonByName" resultType="com.letere.bean.Person">
select *
from `person`
where name = #{name}
</select>
</mapper>
1
2
3
4
5
6
7
8
9
10
11
Service调用Dao层
@Service
public class PersonService implements PersonMapper {
@Autowired
PersonMapper personMapper;

@Override
public Person queryPersonByName(String name) {
return personMapper.queryPersonByName(name);
}
}

6.4 修改Relam中的认证方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行 ==> 认证doGetAuthenticationInfo");

//(2)实现数据库认证
UsernamePasswordToken userToken = (UsernamePasswordToken)token;
Person person = personService.queryPersonByName(userToken.getUsername()); //通过用户名查询对象
if (person == null){ //对象为空,即查询失败,不存在此用户名
return null;
}

//密码认证Shiro会自动执行
return new SimpleAuthenticationInfo(person, person.getPassword(), ""); //参数1传bean对象
}

七、授权方法实现

授予用户特定的权限去访问特定的页面

7.1 添加需要权限访问的页面

在ShiroConfig的配置类中的拦截中,添加需要权限才能访问的页面
并且可以设置权限不够时,自动跳转的页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  @Bean(name = "shiroFilterFactoryBean") //要起别名为"shiroFilterFactoryBean",否则报错
public ShiroFilterFactoryBean getFilterFactoryBean(DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
filterFactoryBean.setSecurityManager(securityManager);
//---------------------里面配置相关设置---------------------------
//添加shiro内容过滤器
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/toAdd", "anon");
filterMap.put("/user/toUpdate", "authc");
filterMap.put("/user/toDelete", "perms[admin]"); //需要admin权限才能访问

filterFactoryBean.setFilterChainDefinitionMap(filterMap);

filterFactoryBean.setLoginUrl("/toLogin");

//未授权请求,自动跳转到相应的页面
filterFactoryBean.setUnauthorizedUrl("/unauthorized");
//------------------------------------------------------------------------
return filterFactoryBean;
}

7.2 整合数据库实现

  • (1)数据库添加新字段

在数据库中添加新的字段perms,来表示该用户拥有的权限

1
2
alter table `person`
add perms varchar(20)
  • (2)修改对应的Bean类
  • (3)修改Relam类中的授权方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("执行 ==> 授权doGetAuthorizationInfo");

SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

//获取验认证通过后的用户
Person person = (Person)principals.getPrimaryPrincipal();
//设置用户权限
authorizationInfo.addStringPermission(person.getPerms());

return authorizationInfo;
}

八、Shiro整合thymeleaf

整合thymeleaf,实现根据用户的权限,来显示其能够访问的页面

8.1 导入相关依赖

1
2
3
4
5
6
<!--shiro与thymeleaf整合-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>

8.2 修改ShiroConfig类

在Shiro配置类中,住一个ShiroDialect类的Bean

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

8.3 修改前端代码


9 修改过滤器(拦截器)

9.1 介绍

  • shiro对需要验证的请求authc,进行拦截,并进行判断是否已登录等操作或其他操作
  • 这些操作都在shiro的默认过滤器中处理
  • 我们可以自定义一个过滤器,来替换shiro默认的过滤器,来实现我们自己想要的业务

9.2 实现

  • (1)创建过滤器
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //继承org.apache.shiro.web.filter.authc.FormAuthenticationFilter类
    public class MyShiroFilter extends FormAuthenticationFilter {

    //重写onAcessDeniend方法
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
    //业务逻辑代码

    return true; //放行
    return false; //拦截
    }
    }
  • (2)替换过滤器
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Configuration
    public class ShiroConfig extends ShiroWebFilterConfiguration {

    //......

    @Override
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
    ShiroFilterFactoryBean shiroFilterFactoryBean = super.shiroFilterFactoryBean();

    //使用自定义过滤器,来替换authc自带的过滤器
    Map<String, Filter> filters = new HashMap<>();
    filters.put("authc", new MyShiroFilter());
    shiroFilterFactoryBean.setFilters(filters);

    return shiroFilterFactoryBean;
    }
    }