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 功能介绍
基本功能点如下图所示:

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

==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页面和查询功能可以在不登录下直接访问,增删改页面必须登录

开发流程

①:依赖准备,页面准备,Controller页面跳转准备
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
|

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
|
<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(); 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路径可以正常访问,但是增删改的路径直接会报错


⑥: 访问增删改报错时直接跳转到默认的login.jsp页面,因此需要在代码中配置登录页面
1 2
| shiroFilterFactoryBean.setLoginUrl("/movie/toLogin");
|
再次访问后,跳转到指定页面

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
| 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; } Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username, password); 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"; UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken; if(!StrUtil.equals(username, token.getUsername())){ return null; } SimpleAuthenticationInfo userRealm = new SimpleAuthenticationInfo(username, password, "userRealm"); System.out.println("执行认证逻辑"); return userRealm; } }
|
⑤:测试


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

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); } Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username, password); 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
目前用户名和密码都是人为创建的,需要连接数据库查询用户表,获取真实数据。
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); }
|
MD5加密的弊端主要有以下几点:
容易被暴力破解:由于MD5算法是单向加密算法,无法反向解密,因此黑客可以通过暴力破解方式,不断尝试各种可能的明文密码,最终得到加密后的MD5值,并通过字典攻击等方法进行密码猜测。
容易被碰撞攻击:MD5算法存在碰撞攻击的漏洞,即通过构造不同的输入明文,可以得到相同的MD5值。这种情况下,黑客可以利用碰撞攻击获得另一个用户的密码,进而进行攻击。
已被证明不安全:MD5算法已被广泛认为不安全,许多网站已经禁用了MD5加密算法,并采用更加安全的算法如SHA-256、SHA-512等。
因此,建议在实际应用中,不要单独使用MD5加密算法来保护用户密码,而应该采用更加安全的加密算法,并加入盐值等技术,以提高安全性。

导入ShiroUtils工具类,内部设置有md5算法以及盐的生成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public static void main(String[] args) { 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); 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]");
|

1 2
| shiroFilterFactoryBean.setUnauthorizedUrl("/movie/unauthorizedurl");
|

1 2 3 4 5 6
| protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleAuthorizationInfo sim = new SimpleAuthorizationInfo(); sim.addStringPermission("movie_delete"); System.out.println("执行授权逻辑"); return sim; }
|