shiro1

Apache Shiro

1.开始使用 Apache Shiro

认证() 授权

Apache Shiro 是 Java 的一个安全(权限)框架。Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE 环境,也可以用在 JavaEE 环境。

通俗易讲来说,Shiro就是用来实现登录验证及权限管理的。在java中,实现权限管理可通过以下几种方式进行实现:

1、数据库表设计

2、Shiro框架

3、Spring Security框架

官网位置:http://shiro.apache.org/

1.1 功能介绍

基本功能点如下图所示:

1681455691814

角色 作用
Authentication 身份认证/登录,验证用户是不是拥有相应的身份
Authorization 授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作
Session Manager 会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境,也可以是 Web 环境的
Cryptography 加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储
Web Support Web 支持,可以非常容易的集成到Web 环境
Caching 缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率
Concurrency Shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能 把权限自动传播过去
Testing 提供测试支持
Run As 允许一个用户假装为另一个用户(如果他们允许)的身份进行访问

1.2 Shiro架构

Shiro 的架构有 3 个主要概念:Subject,SecurityManagerRealm

1681458373914

  • ==Subject==:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外API 核心就是 Subject。

    Subject 代表了当前“用户”, 这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,与 Subject 的所有交互都会委托给 SecurityManager;Subject 其实是一个门面,SecurityManager 才是实际的执行者

  • ==SecurityManager==:即所有Subject的管理者,这是Shiro框架的核心组件,可以把他看做是一个Shiro框架的全局管理组件,用于调度各种Shiro框架的服务。作用类似于SpringMVC中的DispatcherServlet,用于拦截所有请求并进行处理。

  • ==Realm==: Realm是用户的信息认证器和用户的权限证器,我们需要自己来实现Realm来自定义的管理我们自己系统内部的权限规则。SecurityManager要验证用户,需要从Realm中获取用户。可以把Realm看做是数据源。


1.3 ShiroFilterFactoryBean类

在使用 Shiro 框架时,我们可以定义一组过滤器链规则,用于控制哪些请求需要进行身份验证,哪些请求需要具有特定的角色或权限才能访问等等。

提供的参数:

  • anon:匿名访问过滤器,即不需要进行认证即可访问的资源。

  • authc:基于表单的身份验证过滤器,需要用户输入用户名和密码进行认证

  • logout:用户退出过滤器,处理用户退出登录的请求。

  • roles:角色过滤器,需要用户具有指定角色才能访问资源。

  • perms:权限过滤器,需要用户具有指定权限才能访问资源。

  • ssl:SSL 安全连接过滤器,需要通过 HTTPS 协议访问资源。

  • port:端口过滤器,需要通过指定端口访问资源。

​ 这些参数可以使用DefaultFilter 提供的枚举类获取,也可以写入字符串。


2.Shiro入门程序

2.1 简单Shiro程序

使用Shiro完成增删改查页面路径过滤操作,要求index页面和查询功能可以在不登录下直接访问,增删改页面必须登录

  • 访问流程

1681488603780

  • 开发流程

    1681475616035

①:依赖准备,页面准备,Controller页面跳转准备

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

1681473803405

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
package com.cjc.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
*/
@Controller
@RequestMapping("/movie")
public class MovieController {

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

@RequestMapping("/delete")
public String delete(){
return "delete";
}

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

@RequestMapping("/select")
public String select(){
return "select";
}

@RequestMapping("/index")
public String index(){
return "index";
}
}

②:导入Shiro依赖

1
2
3
4
5
6
7

<!-- 支持springboot2.7.10 springboot3.3.4: shiro: 2.x -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>

③:编写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
25
package com.cjc.realm;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

/**
*/
public class UserRealm extends AuthorizingRealm {
@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
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
package com.cjc.config;

import com.cjc.realm.UserRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.mgt.DefaultFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.FilterRegistration;
import java.util.LinkedHashMap;

/**
*/
@Configuration
public class ShiroConfig {

@Bean
public UserRealm userRealm(){
return new UserRealm();
}

@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//关联UserRealm
defaultWebSecurityManager.setRealm(userRealm());
return defaultWebSecurityManager;
}

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager());
//设置拦截路径
LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put("/movie/index", DefaultFilter.anon.name());
map.put("/movie/select", DefaultFilter.anon.name());
map.put("/**", DefaultFilter.authc.name());

//放入过滤器中
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
}

⑤:测试

发现index和select路径可以正常访问,但是增删改的路径直接会报错

1681474667588

1681474699158

⑥: 访问增删改报错时直接跳转到默认的login.jsp页面,因此需要在代码中配置登录页面

1
2
//设置登录页面
shiroFilterFactoryBean.setLoginUrl("/movie/toLogin");

​ 再次访问后,跳转到指定页面

1681474940842

2.2 实现用户认证

l导入hutool依赖

1
2
3
4
5
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.10</version>
</dependency>

①:编写login.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="/js/vue.js"></script>
<script src="/js/axios.js"></script>
</head>
<body>

<div id="app">
<form>
账号:<input type="text" v-model="user.username"><br/>
密码: <input type="password" v-model="user.password"> <br/>
<input type="button" @click="loginBtn" value="登陆">
</form>
</div>

<script type="text/javascript">
new Vue({
el: "#app",
data: {
user:{
username:'',
password:'',
}
},
methods:{
loginBtn(){
axios.post("/movie/login",this.user)
.then(resp=>{
if(resp.data.code==1){
// 登陆成功,
alert("登陆成功");
location.href="/movie/index";
}else{
alert(resp.data.message);
}
}).catch(resp=>{

})
},
}
});
</script>
</body>
</html>

②:放行登录页面和静态js资源

1
2
3
4
//放开js静态资源
map.put("/js/**", DefaultFilter.anon.name());
//放行登录页面
map.put("/movie/login",DefaultFilter.anon.name());

③: 编写控制层

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
package com.cjc.controller;

import cn.hutool.Hutool;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
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.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

/**
*/
@Controller
@RequestMapping("/movie")
public class MovieController {

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

@RequestMapping("/delete")
public String delete(){
return "delete";
}

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

@RequestMapping("/select")
public String select(){
return "select";
}

@RequestMapping("/index")
public String index(){
return "index";
}

@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}

@RequestMapping("/login")
@ResponseBody
public Map<String,String> login(String username,String password){
HashMap<String, String> map = new HashMap<>();
//判断用户名和密码是否为空
if(StrUtil.isEmpty(username)||StrUtil.isEmpty(password)){
map.put("code", "1");
map.put("data", "用户名或密码不能为空");
return map;
}
//如果不为空使用shiro进行认证
//1.获取subject
Subject subject = SecurityUtils.getSubject();
//2.封装数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//3.校验
try {
subject.login(token);
map.put("code", "2");
map.put("data", "登录成功");
return map;
} catch (UnknownAccountException e) {
map.put("code", "3");
map.put("data", "用户名或密码错误");
return map;
}catch (IncorrectCredentialsException e){
map.put("code", "3");
map.put("data", "用户名或密码错误");
return map;
}

}
}

④:编写UserRealm认证规则

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
package com.cjc.realm;

import cn.hutool.core.util.StrUtil;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

/**
*/
public class UserRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行授权逻辑");
return null;
}

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//模拟数据库数据
String username = "zhangsan";
String password = "123456";
//拦截到controller认证请求
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
//判断用户名是否存在
if(!StrUtil.equals(username, token.getUsername())){
return null;
}
//校验密码
SimpleAuthenticationInfo userRealm = new SimpleAuthenticationInfo(username, password, "userRealm");
System.out.println("执行认证逻辑");
return userRealm;
}
}

⑤:测试

  • 访问增删改跳转到登录页面

1681486384158

  • 用户名或密码输出错误给出提示

1681486427456

  • 用户名或密码输入正确 跳转到index页面正常访问增删改功能

1681486763060

2.3用户认证返回值优化

如果使用map作为返回值代码过于冗余,并且代码中出现大量汉字提示,应该使用对象封装返回信息以及使用常量类定义返回数据。

  • 定义返回值类

    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
    package com.cjc.result;

    /**
    */
    public class ResponseResult {
    private String code;
    private String data;

    public ResponseResult(String code, String data) {
    this.code = code;
    this.data = data;
    }

    public String getCode() {
    return code;
    }

    public void setCode(String code) {
    this.code = code;
    }

    public String getData() {
    return data;
    }

    public void setData(String data) {
    this.data = data;
    }
    }

  • 定义常量接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package com.cjc.result;

    /**
    */
    public interface ResultConstant {
    String USERNAME_PASSWORD_NULL_ERROR_CODE="1";
    String LOGIN_SUCCESS_CODE="2";
    String USERNAME_PASSWORD_ERROR_CODE="3";


    String USERNAME_PASSWORD_NULL_ERROR_DATA="用户名或密码不能为空";
    String LOGIN_SUCCESS_DATA="登录成功";
    String USERNAME_PASSWORD_ERROR_DATA="用户名或密码错误";
    }

  • 修改controller返回值代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @RequestMapping("/login")
    @ResponseBody
    public ResponseResult login(String username, String password){
    //判断用户名和密码是否为空
    if(StrUtil.isEmpty(username)||StrUtil.isEmpty(password)){
    return new ResponseResult(ResultConstant.USERNAME_PASSWORD_NULL_ERROR_CODE, ResultConstant.USERNAME_PASSWORD_NULL_ERROR_DATA);
    }
    //如果不为空使用shiro进行认证
    //1.获取subject
    Subject subject = SecurityUtils.getSubject();
    //2.封装数据
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    //3.校验
    try {
    subject.login(token);
    return new ResponseResult(ResultConstant.LOGIN_SUCCESS_CODE, ResultConstant.LOGIN_SUCCESS_DATA);
    } catch (UnknownAccountException e) {
    return new ResponseResult(ResultConstant.USERNAME_PASSWORD_ERROR_CODE, ResultConstant.USERNAME_PASSWORD_ERROR_DATA);
    }catch (IncorrectCredentialsException e){
    return new ResponseResult(ResultConstant.USERNAME_PASSWORD_ERROR_CODE, ResultConstant.USERNAME_PASSWORD_ERROR_DATA);
    }

    }

2.4整合Mybatis

目前用户名和密码都是人为创建的,需要连接数据库查询用户表,获取真实数据。

  • 整合mybatis相关依赖,配置等信息集成mybatis开发环境

  • 修改UserRealm代码

    1
    2
    //查询数据
    User user = userService.findUsername(token.getUsername());
  • 修改service实现类代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Service
    public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public User findUsername(String username) {
    UserExample userExample = new UserExample();
    userExample.createCriteria().andUsernameEqualTo(username);
    List<User> users = userMapper.selectByExample(userExample);
    if(users!=null&&users.size()>0){
    return users.get(0);
    }
    return null;
    }
    }

2.5密码加密

数据库中存储着许多敏感信息,例如我们的账户、密码、信用卡号码等等。如果这些信息的安全性得不到保障,黑客可能会通过窃取密码等方式获取这些信息,并进行不良用途,比如盗刷银行卡、冒充身份等等。为了避免这种情况发生,数据库管理员需要对数据库中的密码进行加密。

哈希加密(Hash Encryption):将明文密码通过哈希算法处理成一段固定长度的密文,常用的哈希算法有MD5、SHA-1、SHA-2等。

1
2
3
4
5
6
7
8
public static void main(String[] args) {
String s = MD5.create().digestHex("1234");
System.out.println(s);
String s1 = MD5.create().digestHex("1234");
System.out.println(s1);
}
//81dc9bdb52d04dc20036dbd8313ed055
//81dc9bdb52d04dc20036dbd8313ed055

MD5加密的弊端主要有以下几点:

  1. 容易被暴力破解:由于MD5算法是单向加密算法,无法反向解密,因此黑客可以通过暴力破解方式,不断尝试各种可能的明文密码,最终得到加密后的MD5值,并通过字典攻击等方法进行密码猜测。

  2. 容易被碰撞攻击:MD5算法存在碰撞攻击的漏洞,即通过构造不同的输入明文,可以得到相同的MD5值。这种情况下,黑客可以利用碰撞攻击获得另一个用户的密码,进而进行攻击。

  3. 已被证明不安全:MD5算法已被广泛认为不安全,许多网站已经禁用了MD5加密算法,并采用更加安全的算法如SHA-256、SHA-512等。

因此,建议在实际应用中,不要单独使用MD5加密算法来保护用户密码,而应该采用更加安全的加密算法,并加入盐值等技术,以提高安全性。

1681613258929

导入ShiroUtils工具类,内部设置有md5算法以及盐的生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
//生成6位盐值
String salt = generateSalt(6);
System.out.println("盐:"+salt);
//密码加入盐值后统一加密
String password= "1234";
String s = encryptPassword("md5", password, salt, 5);
System.out.println(s);

String salt1 = generateSalt(6);
System.out.println("盐:"+salt1);
String password1= "1234";
String s1 = encryptPassword("md5", password, salt1, 5);
System.out.println(s1);
}
1
2
3
4
盐:b97b33
4567dd4ac09b00dbe8fcb612ac6e266f
盐:cc3fa3
f76729ce4a7de7166f9553e6c194842f

在数据库中加入salt字段并且完成加密和解密功能

①:修改ShiroConfig增加加密配置并在userRealm()中增加加密规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hash = new HashedCredentialsMatcher();
//指定加密方式
hash.setHashAlgorithmName("md5");
//指定加密次数
hash.setHashIterations(7);
//指定编码(是否存储为16进制)
hash.setStoredCredentialsHexEncoded(true);

return hash;
}

@Bean
public UserRealm userRealm(){
UserRealm userRealm = new UserRealm();
userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return userRealm;
}

②:修改UserRealm代码

1
SimpleAuthenticationInfo userRealm = new SimpleAuthenticationInfo(user,user.getPassword(), ByteSource.Util.bytes("b97b33"), "userRealm");

2.6 获取登录成功后的用户信息

1
2
3
4
5
6
@RequestMapping("/add")
public String insert(){
User user = (User)SecurityUtils.getSubject().getPrincipal();
System.out.println(user);
return "add";
}

2.7 退出登录

直接在过滤器配置中过滤路径并设置为logout类型即可,默认跳转到index首页 /

1
2
//退出登录
map.put("/movie/logout", DefaultFilter.logout.name());

如果想配置其他路径,在shiro配置文件中加入如下配置

1
2
3
4
5
Map<String, Filter> filters = new LinkedHashMap<>();
LogoutFilter logoutFilter = new LogoutFilter();
logoutFilter.setRedirectUrl("/movie/toLogin");
filters.put("logout", logoutFilter);
shiroFilterFactoryBean.setFilters(filters);

3 权限控制

需求:对于增删改页面,增和改页面需要登录后才能访问,但是删除页面除了登录还需要访问权限。

  • 增加授权配置
1
2
//删除页面授权才可使用
map.put("/movie/delete", "perms[movie_delete]");

1681616631149

  • 增加授权页面跳转配置
1
2
//设置没有权限页面
shiroFilterFactoryBean.setUnauthorizedUrl("/movie/unauthorizedurl");

1681616723990

  • 编写UserRealm授权代码,正常访问页面
1
2
3
4
5
6
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo sim = new SimpleAuthorizationInfo();
sim.addStringPermission("movie_delete");
System.out.println("执行授权逻辑");
return sim;
}