(1)Authentication:身份认证/登录,验证用户是不是拥有相应的身份
(2)Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即 判断用 户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证 某个用户 对某个资源是否具有某个权限;
(3)Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的 所有 信息都在会话中;会话可以是普通 JavaSE 环境,也可以是 Web 环境的;
(4)Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存 储;
(5)Web Support:Web 支持,可以非常容易的集成到 Web 环境;
(6)Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这 样可 以提高效率;
(7)Concurrency:Shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线 程,能把权限自动传播过去;
(8)Testing:提供测试支持;
(9)Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问; (10)Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用 登 录了
(1)Subject:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心 就是 Subject。Subject 代表了当前“用户”, 这个用户不一定 是一个具体的人,与当 前应用交互的任何东西都是 Subject,如网络爬虫, 机器人等;与 Subject 的所有交互 都会委托给 SecurityManager; Subject 其实是一个门面,SecurityManager 才是实际的 执行者;
(2)SecurityManager:安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且其管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与 Shiro 的其他 组件进行交互,它相当于 SpringMVC 中 DispatcherServlet 的角色
(3)Realm:Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户 进行比较以确 定用户身份是否合法;也需要从 Realm 得到用户相应的角色/ 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource
(1)Subject:任何可以与应用交互的“用户”
(2)SecurityManager :相当于 SpringMVC 中的 DispatcherServlet;是 Shiro 的心 脏; 所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负 责进 行认证、授权、会话及缓存的管理。
(3)Authenticator:负责 Subject 认证,是一个扩展点,可以自定义实现;可以使用认 证 策略(Authentication Strategy),即什么情况下算用户认证通过了;
(4)Authorizer:授权器、即访问控制器,用来决定主体是否有权限进行相应的操作;即 控 制着用户能访问应用中的哪些功能;
(5)Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是内存实现等等;由用户提供;所以一般在应用中都需 要 实现自己的 Realm;
(6)SessionManager:管理 Session 生命周期的组件;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境
(7)CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据 基本上很少改变,放到缓存中后可以提高访问的性能
(8)Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密/解 密。
<dependencies>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-coreartifactId>
<version>1.9.0version>
dependency>
<dependency>
<groupId>commons-logginggroupId>
<artifactId>commons-loggingartifactId>
<version>1.2version>
dependency>
dependencies>
Shiro获取权限相关的信息可以通过数据库获取,这里为了方便通过ini配置文件获取
就是把账号密码写在这个文件里就可
(1)身份验证:一般需要提供如身份ID等一些标识信息来表明登录者的身份,如提供 email,用户名/密码来证明。
(2)在shiro中,用户需要提供principals(身份)和credentials(证明)给shiro,从 而应用能验证用户身份:
(3)principals:身份,即主体的标识属性,可以是任何属性,如用户名、邮箱等,唯一 即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/ 邮箱/手机号。
(4)credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
(5)最常见的principals和credentials组合就是用户名/密码
(1)收集用户身份/凭证,即如用户名/密码
(2)调用 Subject.login 进行登录,如果失败将得到相应 的 AuthenticationException 异常,根据异常提示用户 错误信息;否则登录成功
(3)创建自定义的 Realm 类,继承 org.apache.shiro.realm.AuthenticatingRealm类, 实现 doGetAuthenticationInfo() 方法
public class ShiroRun {
public static void main(String[] args) {
//1、初始化获取SecurityManager
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
//将安全管理器放到工具类当中,之后可以通过工具类获取subject对象
SecurityUtils.setSecurityManager(securityManager);
//2、获取Subject对象
Subject subject = SecurityUtils.getSubject();
//3、创建token对象,web应用用户名和密码从页面传递
AuthenticationToken token = new UsernamePasswordToken("zhangsan","z3");
//4、完成登录
try {
subject.login(token);
System.out.println("登录成功");
}catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户不存在");
}
catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
}
catch (AuthenticationException e) {
//unexpected condition? error?
e.printStackTrace();
System.out.println("登录失败");
}
}
}
(1)首先调用 Subject.login(token) 进行登录,其会自动委托给 SecurityManager
(2)SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份 验证;
(3)Authenticator 才是真正的身份验证者,Shiro API 中核心的身份 认证入口点,此 处可以自定义插入自己的实现;
(4)Authenticator 可能会委托给相应的 AuthenticationStrategy 进 行多 Realm 身份 验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
(5) Authenticator 会把相应的 token 传入 Realm,从 Realm 获取 身份验证信息,如 果没有返回/抛出异常表示身份验证失败了。此处 可以配置多个Realm,将按照相应的顺序 及策略进行访问。
(1)授权,也叫访问控制,即在应用中控制谁访问哪些资源(如访问页面/编辑数据/页面 操作 等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权 限 (Permission)、角色(Role)。
(2)主体(Subject):访问应用的用户,在 Shiro 中使用 Subject 代表该用户。用户只 有授权 后才允许访问相应的资源。
(3)资源(Resource):在应用中用户可以访问的 URL,比如访问 JSP 页面、查看/编辑 某些 数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。
(4)权限(Permission):安全策略中的原子授权单位,通过权限我们可以表示在应用中 用户 有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如:访 问用 户列表页面查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权 限控 制)等。权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允 不允 许。
(5)Shiro 支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权 限, 即实例级别的)
(6)角色(Role):权限的集合,一般情况下会赋予用户角色而不是权限,即这样用户可 以拥有 一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工 程师等 都是角色,不同的角色拥有一组不同的权限
(1)编程式:通过写if/else 授权代码块完成
(2)注解式:通过在执行的Java方法上放置相应的注解完成,没有权限将抛出相应的异常
(3)JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成
(1)首先调用Subject.isPermitted*/hasRole*接口,其会委托给SecurityManager,而 SecurityManager接着会委托给 Authorizer;
(2)Authorizer是真正的授权者,如果调用如isPermitted(“user:view”),其首先会通 过PermissionResolver把字符串转换成相应的Permission实例;
(3)在进行授权之前,其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入 的角色/权限;
(4)Authorizer会判断Realm的角色/权限是否和传入的匹配,如果有多个Realm,会委托 给ModularRealmAuthorizer进行循环判断,如果匹配如isPermitted*/hasRole* 会返回 true,否则返回false表示授权失败
配置角色信息和权限
在登录之后,就可以使用subject进行判断
subject.login(token);
System.out.println("登录成功");
//5判断角色
boolean hasRole1 = subject.hasRole("role1");
System.out.println("是否拥有此角色 role1 = " + hasRole1);
//6 判断权限
boolean isPermitted = subject.isPermitted("user:insert");
System.out.println("是否拥有此权限 user:insert = " + isPermitted);
//也可以使用checkPermission方法,这个方法没有返回值,没有权限的话直接抛出异常AuthenticationException
对密码加密,Shiro中内嵌了很多常用的加密算法
public class ShiroMD5 {
public static void main(String[] args) {
//密码明文
String password = "z3";
//使用MD5加密
Md5Hash md5Hash = new Md5Hash(password);
System.out.println("md5加密:"+md5Hash);
//带盐的MD5加密,盐就是在密码后买拼接字符串,然后再进行加密
Md5Hash md5Hash2 = new Md5Hash(password,"salt");
System.out.println("md5带盐加密:"+md5Hash2);
//为了保证安全,避免被破解还可以多次迭代加密,保证数据安全
//这里加密三次
Md5Hash md5Hash3 = new Md5Hash(password,"salt",3);
System.out.println("md5带盐三次加密:"+md5Hash3.toHex());
//使用父类进行加密,区别就是可以指定加密的算法
SimpleHash simpleHash = new SimpleHash("MD5",password,"salt",3);
System.out.println("通过父类md5带盐三次加密:"+simpleHash.toHex());
}
}
[main]
md5CredentialsMatcher=org.apache.shiro.authc.credential.Md5CredentialsMatcher
md5CredentialsMatcher.hashIterations=3
myrealm=com.zylai.shirotest.MyRealm
myrealm.credentialsMatcher=$md5CredentialsMatcher
securityManager.realms=$myrealm
[users]
zhangsan=7174f64b13022acd3c56e2781e098a5f,role1,role2
lisi=l4
[roles]
role1=user:insert,user:select
public class MyRealm extends AuthenticatingRealm {
//自定义的登录认证方法,Shiro的login方法底层会调用该类的认证方法进行认证
//还需要通过配置使自定义的realm生效,目前在ini文件中配置,之后在springboot中配置
//该方法只是获取进行对比的信息,认证逻辑还是按照Shiro底层认证逻辑完成
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1、获取身份信息
String principal = token.getPrincipal().toString();
//2、获取凭证信息
String password = new String((char[]) token.getCredentials());
System.out.println("认证用户的信息:"+principal+"---"+password);
//3、访问数据库获取数据库存储的用户信息,这里暂时不去访问数据库
if(principal.equals("zhangsan")){
// 3.1数据库中年存储的肯定不是明文,是加盐迭代三次的密码
String pwdInfo = "7174f64b13022acd3c56e2781e098a5f";
//4、封装到校验逻辑对象,把这个对象返回即可
AuthenticationInfo info = new SimpleAuthenticationInfo(
token.getPrincipal(),
pwdInfo,
ByteSource.Util.bytes("salt"),
token.getPrincipal().toString()
);
return info;
}
return null;
}
}
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.1.RELEASEversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>shiro_springbootartifactId>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-spring-boot-web-starterartifactId>
<version>1.9.0version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.0.5version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.29version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
dependencies>
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:mapper/*.xml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&useSSL=false
username: root
password: root
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
shiro:
loginUrl: /myController/login
@SpringBootApplication
@MapperScan("com.zylai.shiro.mapper")
public class ShiroApplication {
public static void main(String[] args) {
SpringApplication.run(ShiroApplication.class,args);
}
}
这一步就是业务真正实现
(1)数据库表
CREATE TABLE `user` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
`name` VARCHAR(30) DEFAULT NULL COMMENT '用户名',
`pwd` VARCHAR(50) DEFAULT NULL COMMENT '密码',
`rid` BIGINT(20) DEFAULT NULL COMMENT '角色编号',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='用户表';
(2)实体类
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
private Integer id;
private String name;
private String pwd;
private Integer rid;
}
(3)mapper
public interface UserMapper extends BaseMapper<User> {
(4)service
public interface UserService {
//用户登录
User getUserInfoByName(String name);
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User getUserInfoByName(String name) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name",name);
return userMapper.selectOne(wrapper);
}
}
@Configuration
public class ShiroConfig {
//注入自定理的realm
@Autowired
private MyRealm myRealm;
//配置SecurityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
//1、创建defaultWebSecurityManager 对象
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//2、创建加密对象,设置相关属性
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
//2.1 采用md5加密
matcher.setHashAlgorithmName("md5");
//2.2 迭代加密的次数
matcher.setHashIterations(3);
//3、将加密对象存储到myRealm中
myRealm.setCredentialsMatcher(matcher);
//4、将myRealm存入defaultWebSecurityManager对象
defaultWebSecurityManager.setRealm(myRealm);
//5、返回
return defaultWebSecurityManager;
}
//配置 Shiro 内置过滤器拦截范围
@Bean
public DefaultShiroFilterChainDefinition
shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition definition = new
DefaultShiroFilterChainDefinition();
//设置不认证可以访问的资源
definition.addPathDefinition("/myController/userLogin","anon");
definition.addPathDefinition("/login","anon");
//设置需要进行登录认证的拦截范围
definition.addPathDefinition("/**","authc");
return definition;
}
}
这里就是对应着原生操作中自定义登录认证那一步
@Component
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
//自定义授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
//自定义登录方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1、获取用户信息
String username = token.getPrincipal().toString();
//2、调用业务层获取用户信息(数据库中)
User user = userService.getUserInfoByName(username);
//3、非空值判断,将数据封装返回
if(user != null){
/**
* 1、用户身份信息
* 2、用户在数据中存储的密码(加密之后的)
* 3、盐值
* 4、用户名
*/
AuthenticationInfo info = new SimpleAuthenticationInfo(
token.getPrincipal(),
user.getPwd(),
ByteSource.Util.bytes("salt"),
username
);
return info;
}
return null;
}
}
这里和原生的不同之处在于少了一步 初始化获取SecurityManager,并把SecurityManager发入工具类中
,因为我们已经通过配置类配置了SecurityManager
如此,登录的逻辑就很容易了,就下面这三步
@Controller
@RequestMapping("/myController")
public class MyController {
@GetMapping("/userLogin")
@ResponseBody
public String userLogin(String name,String pwd){
//1、获取Subject对象
Subject subject = SecurityUtils.getSubject();
//2、封装请求数据token
AuthenticationToken token = new UsernamePasswordToken(name,pwd);
//3、调用login方法进行登录认证
try {
subject.login(token);
return "登录成功";
} catch (AuthenticationException e) {
System.out.println("登录失败");
return "登录失败";
}
}
}
发送请求:http://localhost:8080/myController/userLogin?name=张三&pwd=z3
自定义Realm中也真正去数据库中查询了
(1)引入thymeleaf依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
(2)创建两个页面
login:
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>Shiro 登录认证h1>
<br>
<form action="/myController/userLogin">
<div>用户名:<input type="text" name="name" value="">div>
<div>密码:<input type="password" name="pwd" value="">div>
<div><input type="submit" value="登录">div>
form>
body>
html>
登录成功之后的:
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>Shiro 登录认证后主页面h1>
<br>
登录用户为:<span th:text="${session.user}">span>
body>
html>
(3)添加controller中的login方法,改造认证的方法
//跳转登录页面
@GetMapping("/login")
public String login(){
return "login";
}
认证方法,登录成功之后跳转到视图页面
@GetMapping("/userLogin")
public String userLogin(String name, String pwd, HttpSession session){
//1、获取Subject对象
Subject subject = SecurityUtils.getSubject();
//2、封装请求数据token
AuthenticationToken token = new UsernamePasswordToken(name,pwd);
//3、调用login方法进行登录认证
try {
subject.login(token);
//将登陆成功的用户名放到session中
session.setAttribute("user",token.getPrincipal().toString());
return "main";
} catch (AuthenticationException e) {
//System.out.println(e.getMessage());
System.out.println("登录失败");
session.setAttribute("user","登录失败");
return "main";
}
}
(4)一定要在配置类中放行登录请求
//配置 Shiro 内置过滤器拦截范围
@Bean
public DefaultShiroFilterChainDefinition
shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition definition = new
DefaultShiroFilterChainDefinition();
//设置不认证可以访问的资源
definition.addPathDefinition("/myController/userLogin","anon");
definition.addPathDefinition("/login","anon");
//设置需要进行登录认证的拦截范围
definition.addPathDefinition("/**","authc");
return definition;
}
(5)在配置文件中声明登录地址
在controller中写一个处理器,我们视图在未登录的情况下访问这个接口
@ResponseBody
@GetMapping("/test1")
public String test1(){
return "test1";
}
在为进行登录认证的情况下,访问之后会重定向到login页面,这是因为在配置文件中的配置
之后通过登录页面登录之后,看一下请求的响应为:
同时存储了session的JSESSIONID
认证成功之后,我们再访问/test1
接口,发现可以正常访问,看一下发送的请求如下
所以说shiro是通过session来存储认证的
SecurityManager来管理多个Realm
当应用程序配置多个 Realm 时,例如:用户名密码校验、手机号验证码校验等等。 Shiro 的 ModularRealmAuthenticator 会使用内部的 AuthenticationStrategy 组件判断认 证是成功还是失败。
AuthenticationStrategy 是一个无状态的组件,它在身份验证尝试中被询问 4 次(这 4 次交互所需的任何必要的状态将被作为方法参数):
(1)在所有 Realm 被调用之前
(2)在调用 Realm 的 getAuthenticationInfo 方法之前
(3)在调用 Realm 的 getAuthenticationInfo 方法之后
(4)在所有 Realm 被调用之后 认证策略的另外一项工作就是聚合所有 Realm 的结果信息封装至一个 AuthenticationInfo 实例中,并将此信息返回,以此作为 Subject 的身份信息。
Shiro 中定义了 3 种认证策略的实现:
AuthenticationStrategy class | 描述 |
---|---|
AtLeastOneSuccessfulStrategy | 只要有一个(或更多)的 Realm 验证成功,那么认证将视为成功 |
FirstSuccessfulStrategy | 第一个 Realm 验证成功,整体认证将视为成功,且后续 Realm 将被忽略 |
AllSuccessfulStrategy | 所有 Realm 成功,认证才视为成功 |
ModularRealmAuthenticator 内置的认证策略默认实现是 AtLeastOneSuccessfulStrategy 方式。可以通过配置修改策略
在配置类中配置SecurityManager的过程中加入认证对象
//配置 SecurityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
//1 创建 defaultWebSecurityManager 对象
DefaultWebSecurityManager defaultWebSecurityManager = new
DefaultWebSecurityManager();
//2 创建认证对象,并设置认证策略
ModularRealmAuthenticator modularRealmAuthenticator = new
ModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(new
AllSuccessfulStrategy());
defaultWebSecurityManager.setAuthenticator(modularRealmAuthenticator)
;
//3 封装 myRealm 集合
List<Realm> list = new ArrayList<>();
list.add(myRealm);
list.add(myRealm2);
//4 将 myRealm 存入 defaultWebSecurityManager 对象
defaultWebSecurityManager.setRealms(list);
//5 返回
return defaultWebSecurityManager;
}
这里不再举具体的例子
Shiro 提供了记住我(RememberMe)的功能,比如访问一些网站时,关闭了浏览器, 下次再打开时还是能记住你是谁, 下次访问时无需再登录即可访问。
(1) 首先在登录页面选中 RememberMe 然后登录成功;如果是浏览器登录,一般会 把 RememberMe 的 Cookie 写到客户端并保存下来;
(2) 关闭浏览器再重新打开;会发现浏览器还是记住你的;
(3) 访问一般的网页服务器端,仍然知道你是谁,且能正常访问;
(4) 但是,如果我们访问电商平台时,如果要查看我的订单或进行支付时,此时还 是需要再进行身份认证的,以确保当前用户还是你。
(1)在配置类中进行修改
添加两个方法,分别为设置cookie的属性和 创建Shiro 的 cookie 管理对象
//cookie 属性设置
public SimpleCookie rememberMeCookie(){
SimpleCookie cookie = new SimpleCookie("rememberMe");
//设置跨域
//cookie.setDomain(domain);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(30*24*60*60);
return cookie;
}
//创建 Shiro 的 cookie 管理对象
public CookieRememberMeManager rememberMeManager(){
CookieRememberMeManager cookieRememberMeManager = new
CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
//rememberMe 对应cookie的加密密匙
cookieRememberMeManager.setCipherKey("1234567890987654".getBytes());
return cookieRememberMeManager;
}
然后在配置SecurityManager的过程中加上第4.5步
//配置SecurityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
//1、创建defaultWebSecurityManager 对象
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//2、创建加密对象,设置相关属性
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
//2.1 采用md5加密
matcher.setHashAlgorithmName("md5");
//2.2 迭代加密的次数
matcher.setHashIterations(3);
//3、将加密对象存储到myRealm中
myRealm.setCredentialsMatcher(matcher);
//4、将myRealm存入defaultWebSecurityManager对象
defaultWebSecurityManager.setRealm(myRealm);
//4.5 设置rememberMe
defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
//5、返回
return defaultWebSecurityManager;
}
第三步添加存在用户的过滤器
在创建Shiro过滤器方法中修改
//配置 Shiro 内置过滤器拦截范围
@Bean
public DefaultShiroFilterChainDefinition
shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition definition = new
DefaultShiroFilterChainDefinition();
//设置不认证可以访问的资源
definition.addPathDefinition("/myController/userLogin","anon");
definition.addPathDefinition("/login","anon");
//设置需要进行登录认证的拦截范围
definition.addPathDefinition("/**","authc");
//添加存在用户的过滤器(rememberMe)
definition.addPathDefinition("/**","user");
return definition;
}
(2)修改controller
在认证的方法中,获取remember这一个参数
@GetMapping("/userLogin")
public String userLogin(String name, String pwd,
@RequestParam(defaultValue = "false")boolean rememberMe,
HttpSession session){
//1、获取Subject对象
Subject subject = SecurityUtils.getSubject();
//2、封装请求数据token,同时设置rememberMe
AuthenticationToken token = new UsernamePasswordToken(name,pwd,rememberMe);
//3、调用login方法进行登录认证
try {
subject.login(token);
//将登陆成功的用户名放到session中,
session.setAttribute("user",token.getPrincipal().toString());
return "main";
} catch (AuthenticationException e) {
//System.out.println(e.getMessage());
System.out.println("登录失败");
session.setAttribute("user","登录失败");
return "main";
}
}
//登录认证验证 rememberMe
@GetMapping("/userLoginRm")
public String userLogin(HttpSession session) {
session.setAttribute("user","rememberMe");
return "main";
}
(3)在登陆页面添加一个记住我的选项
<body>
<h1>Shiro 登录认证h1>
<br>
<form action="/myController/userLogin">
<div>用户名:<input type="text" name="name" value="">div>
<div>密码:<input type="password" name="pwd" value="">div>
<div>记住用户:<input type="checkbox" name="rememberMe" value="true">div>
<div><input type="submit" value="登录">div>
form>
body>
(1)
直接通过地址访问userLoginRm
http://localhost:8080/myController/userLoginRm
过滤器拦截跳回登录页面
(2)登录勾选记住用户
(3)关闭浏览器之后,再直接访问访问userLoginRm
http://localhost:8080/myController/userLoginRm
访问成功
用户登录后,配套的有登出操作。直接通过Shiro过滤器即可实现登出
<body>
<h1>Shiro 登录认证后主页面h1>
<br>
登录用户为:<span th:text="${session.user}">span>
<br>
<a href="/logout">登出a>
body>
直接在过滤器中配置即可
//配置 Shiro 内置过滤器拦截范围
@Bean
public DefaultShiroFilterChainDefinition
shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition definition = new
DefaultShiroFilterChainDefinition();
//设置不认证可以访问的资源
definition.addPathDefinition("/myController/userLogin","anon");
definition.addPathDefinition("/login","anon");
//配置登出过滤器
definition.addPathDefinition("/logout","logout");
//设置需要进行登录认证的拦截范围
definition.addPathDefinition("/**","authc");
//添加存在用户的过滤器(rememberMe)
definition.addPathDefinition("/**","user");
return definition;
}
这里只是演示shiro的授权、权限控制功能,并不是严格的RBAC权限控制模型
用户登录后,需要验证是否具有指定角色指定权限。Shiro也提供了方便的工具进行判 断。 这个工具就是Realm的doGetAuthorizationInfo方法进行判断。触发权限判断的有两种 方式
(1) 在页面中通过shiro:属性判断 (2) 在接口服务中通过注解@Requires进行判断
通过给接口服务方法添加注解可以实现权限校验,可以加在控制器方法上,也可以加 在业务方法上,一般加在控制器方法上。常用注解如下:
(1)@RequiresAuthentication 验证用户是否登录,等同于方法subject.isAuthenticated()
(2)@RequiresUser 验证用户是否被记忆: 登录认证成功subject.isAuthenticated()为true 登录后被记忆subject.isRemembered()为true
(3)@RequiresGuest 验证是否是一个guest的请求,是否是游客的请求 此时subject.getPrincipal()为null
(4)@RequiresRoles 验证subject是否有相应角色,有角色访问方法,没有则会抛出异常 AuthorizationException。 例如:@RequiresRoles(“aRoleName”) void someMethod(); 只有subject有aRoleName角色才能访问方法someMethod()
(5)@RequiresPermissions 验证subject是否有相应权限,有权限访问方法,没有则会抛出异常 AuthorizationException。 例如:@RequiresPermissions (“file:read”,”wite:aFile.txt”) void someMethod(); subject必须同时含有file:read和wite:aFile.txt权限才能访问方法someMethod()
(1)建表
角色表
CREATE TABLE `role` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`name` varchar(30) DEFAULT NULL COMMENT '角色名',
`desc` varchar(50) DEFAULT NULL COMMENT '描述',
`realname` varchar(20) DEFAULT NULL COMMENT '角色显示名',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3 COMMENT='角色表';
角色-用户映射表
CREATE TABLE `role_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`uid` bigint DEFAULT NULL COMMENT '用户 id',
`rid` bigint DEFAULT NULL COMMENT '角色 id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COMMENT='角色用户映射\r\n表';
(2)业务准备
mapper
//根据用户名查询用户角色信息
@Select("SELECT NAME FROM role WHERE id IN (SELECT rid FROM role_user WHERE uid=(SELECT id FROM USER WHERE NAME=#{principal}))")
List<String> getUserRoleInfoMapper(@Param("principal") String principal);
service
@Override
public List<String> getUserRoleInfo(String name) {
return userMapper.getUserRoleInfoMapper(name);
}
(3)改造MyRealm授权方法
//自定义授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("进入 自定义授权方法");
//1、获取用户信息
String principal = principals.getPrimaryPrincipal().toString();
//2、调用业务层获取用户的角色信息(数据库中)
List<String> roles = userService.getUserRoleInfo(principal);
System.out.println("当前用户角色信息="+roles);
//3、创建对象,封装当前登录用户的角色、权限信息
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(roles);
//3、返回信息
return info;
}
(4)改造前端页面
<body>
<h1>Shiro 登录认证后主页面h1>
<br>
登录用户为:<span th:text="${session.user}">span>
<br>
<a href="/logout">登出a>
<br>
<a href="/myController/userLoginRoles">测试授权a>
body>
(5)测试
(1)建表
权限表:
CREATE TABLE `permissions` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`name` varchar(30) DEFAULT NULL COMMENT '权限名',
`info` varchar(30) DEFAULT NULL COMMENT '权限信息',
`desc` varchar(50) DEFAULT NULL COMMENT '描述',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COMMENT='权限表';
权限角色映射表:
CREATE TABLE `role_ps` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`rid` bigint DEFAULT NULL COMMENT '角色 id',
`pid` bigint DEFAULT NULL COMMENT '权限 id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COMMENT='角色权限映射\r\n表';
(2)业务准备
这里只是用作演示,没有那么严谨
mapper:
@Select({
""
})
List<String> getUserPermissionInfoMapper(@Param("roles")List<String> roles);
service
@Override
public List<String> getUserPermissionInfo(List<String> roles) {
return userMapper.getUserPermissionInfoMapper(roles);
}
(3)改造MyRealm授权方法
//自定义授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("进入 自定义授权方法");
//1、获取用户信息
String principal = principals.getPrimaryPrincipal().toString();
//2、调用业务层获取用户的角色信息(数据库中)
List<String> roles = userService.getUserRoleInfo(principal);
System.out.println("当前用户角色信息="+roles);
List<String> permissions = userService.getUserPermissionInfo(roles);
System.out.println("当前用户的权限信息="+permissions);
//3、创建对象,封装当前登录用户的角色、权限信息
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(roles);
info.addStringPermissions(permissions);
//3、返回信息
return info;
}
(4)改造前端页面
<body>
<h1>Shiro 登录认证后主页面h1>
<br>
登录用户为:<span th:text="${session.user}">span>
<br>
<a href="/logout">登出a>
<br>
<a href="/myController/userLoginRoles">测试授权-角色验证a>
<br>
<a href="/myController/userPermissions">测试授权-权限验证a>
body>
(5)启动测试
当身份认证不通过和权限认证不通过的时候,shiro都会直接抛出异常,因此我们需要对这些异常进行过处理
直接添加一个异常处理类来捕获两个异常即可
@ControllerAdvice
public class PermissionsExceptionHandler {
@ResponseBody
@ExceptionHandler(UnauthorizedException.class)
public String noPermissions(){
return "无权限";
}
@ResponseBody
@ExceptionHandler(AuthenticationException.class)
public String authorizationException(){
return "身份认证失败";
}
}
就是拥有不同角色或权限的用户看到的页面是不一样的,这里提供了一种思想,实际中也都是这样解决的,即判断用户的权限来显示内容。
这里前端用thymeleaf来演示
(1)添加依赖
<dependency>
<groupId>com.github.theborakompanionigroupId>
<artifactId>thymeleaf-extras-shiroartifactId>
<version>2.0.0version>
dependency>
(2)给配置类添加配置
//用于解析 thymeleaf 中的 shiro:相关属性
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
(3)Thymeleaf中常用的shiro:属性
标签 | 说明 |
---|---|
guest | 用户没有身份验证时显示相应信息,即游客访问信息 |
user | 用户已经身份验证/记住我登录后显示相应的信息 |
authenticated | 用户已经身份验证通过,即 Subject.login 登录成功,不是记住我登录的 |
notAuthenticated | 用户已经身份验证通过,即没有调用 Subject.login 进行登录,包括记住我自动登录的 也属于未进行身份验证 |
principal | 相当于((User)Subject.getPrincipals()).getUsername() |
lacksPermission | 如果当前 Subject 没有权限将显示 body 体内容 |
hasRole | 如果当前 Subject 有角色将显示 body 体内容 |
hasAnyRoles | 如果当前 Subject 有任意一个角色(或的关系)将显示 body 体内容 |
lacksRole | 如果当前 Subject 没有角色将显示 body 体内容 |
hasPermission | 如果当前 Subject 有权限将显示 body 体内容 |
其中用的较多的是:hasRole
和hasPermission
,比如:
和
(4)改造前端页面
注意引入相应的命名空间
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>Shiro 登录认证后主页面h1>
<br>
登录用户为:<span th:text="${session.user}">span>
<br>
<a href="/logout">登出a>
<br/>
<a shiro:hasRole="admin" href="/myController/userLoginRoles">测试授权a>
<br>
<a shiro:hasPermission="user:delete" href="/myController/userPermissions">测试授权-权限验证a>
body>
html>
(5)结果
从上面可以看到,客户端发送一次请求我们都需要进行一次判断,判断该请求携带的用户信息是否有权限,需要从数据库中查询,这样会给数据库带来很大的压力,所以考虑到使用缓存
shiro官方提供了整合EhCache缓存,非常方便,但是这个缓存是单机的,对于我们的分布式架构不友好,所以这里使用redis
行吧,我是用springboot整合完成了spring版本的,才发现他们还提供了springboot的starter,救命,所以为了图方便,你可以直接看4.3
由于shiro官方并没有提供整合redis的依赖,我们引入的是github上开源的第三方依赖shiro-redis
该依赖的使用文档为:http://alexxiyang.github.io/shiro-redis/
下面主要是参考的这个依赖的文档
<dependency>
<groupId>org.crazycakegroupId>
<artifactId>shiro-redisartifactId>
<version>3.3.1version>
dependency>
对应的版本关系为:
使用redis的key-value形式存储,所以需要保证用户的身份标识唯一,即principal唯一,就是在SimpleAuthenticationInfo方法中token.getPrincipal()这个值唯一
//自定义登录方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1、获取用户信息
String username = token.getPrincipal().toString();
//2、调用业务层获取用户信息(数据库中)
User user = userService.getUserInfoByName(username);
//3、非空格判断,将数据封装返回
if(user != null){
/**
* 1、用户身份信息
* 2、用户在数据中存储的密码(加密之后的)
* 3、盐值
* 4、用户名
*/
AuthenticationInfo info = new SimpleAuthenticationInfo(
token.getPrincipal(),
user.getPwd(),
ByteSource.Util.bytes("salt"),
username
);
return info;
}
return null;
}
如果这里是一个实体类,比如:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)token;
UserInfo userInfo = new UserInfo();
userInfo.setUsername(usernamePasswordToken.getUsername());
return new SimpleAuthenticationInfo(userInfo, "123456", getName());
}
那么需要保证类UserInfo
中的一个属性唯一,比如设置userId唯一,就可以在配置中设置cacheManager
的属性
<property name="principalIdFieldName" value="userId" />
shiro-redis插件官方在文档中提供了spring原生配置文件中的配置,这里就使用配置类代替配置文件了,具体的参数参考文档即可:http://alexxiyang.github.io/shiro-redis/
其中详细配置参考:http://alexxiyang.github.io/shiro-redis/#configurable-options
这里完全是跟着官方文档做就行了
@Configuration
public class ShiroConfig {
//注入自定理的realm
@Autowired
private MyRealm myRealm;
//配置SecurityManager
//传入sessionManager,cacheManager,会自动从容器中找
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(SessionManager sessionManager, CacheManager cacheManager){
//1、创建defaultWebSecurityManager 对象
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//2、创建加密对象,设置相关属性
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
//2.1 采用md5加密
matcher.setHashAlgorithmName("md5");
//2.2 迭代加密的次数
matcher.setHashIterations(3);
//3、将加密对象存储到myRealm中
myRealm.setCredentialsMatcher(matcher);
//4、将myRealm存入defaultWebSecurityManager对象
defaultWebSecurityManager.setRealm(myRealm);
//4.5 设置rememberMe
defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
//4.6、设置缓存
defaultWebSecurityManager.setSessionManager(sessionManager);
defaultWebSecurityManager.setCacheManager(cacheManager);
//5、返回
return defaultWebSecurityManager;
}
//cookie 属性设置
public SimpleCookie rememberMeCookie(){
SimpleCookie cookie = new SimpleCookie("rememberMe");
//设置跨域
//cookie.setDomain(domain);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(30*24*60*60);
return cookie;
}
//创建 Shiro 的 cookie 管理对象
public CookieRememberMeManager rememberMeManager(){
CookieRememberMeManager cookieRememberMeManager = new
CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
//rememberMe 对应cookie的加密密匙
cookieRememberMeManager.setCipherKey("1234567890987654".getBytes());
return cookieRememberMeManager;
}
//配置 Shiro 内置过滤器拦截范围
@Bean
public DefaultShiroFilterChainDefinition
shiroFilterChainDefinition(){
//略
}
//用于解析 thymeleaf 中的 shiro:相关属性
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
//shiro 的RedisManager
@Bean
public RedisManager redisManager(){
RedisManager redisManager = new RedisManager();
redisManager.setHost("82.157.140.177:6379");
redisManager.setPassword("123321");
return redisManager;
}
//RedisSessionDAO shiro sessionDao层的实现
//原理就是重写 AbstractSessionDAO
@Bean
public RedisSessionDAO redisSessionDAO(RedisManager redisManager){
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager);
return redisSessionDAO;
}
//shiro的sessionManager
@Bean
public DefaultWebSessionManager sessionManager(RedisSessionDAO redisSessionDAO){
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
defaultWebSessionManager.setSessionDAO(redisSessionDAO);
return defaultWebSessionManager;
}
//cacheManager 缓存 redis实现
@Bean
public RedisCacheManager cacheManager(RedisManager redisManager){
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager);
return redisCacheManager;
}
}
注意到我们是在配置类中将redis的地址写死了,这样肯定不好,所以把这些配置都写配置文件中最好
其实很容易实现,就在配置文件中写好,然后在配置类中使用@Value
注解去读取就行了
可以发现,只有登录的时候,才查询了数据库,其他的操作都没有查询数据库
直接引入
<dependency>
<groupId>org.crazycakegroupId>
<artifactId>shiro-redis-spring-boot-starterartifactId>
<version>3.3.1version>
dependency>
配置类中:
直接注入RedisSessionDAO和RedisCacheManager,其中sessionManager要使用RedisSessionDAO,SecurityManager使用sessionManager和RedisCacheManager
//按照名字找到redisSessionDAO
@Resource
private RedisSessionDAO redisSessionDAO;
//按照名字找到redisCacheManager
@Resource
private RedisCacheManager redisCacheManager;
//配置SecurityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(SessionManager sessionManager){
//1、创建defaultWebSecurityManager 对象
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//2、创建加密对象,设置相关属性
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
//2.1 采用md5加密
matcher.setHashAlgorithmName("md5");
//2.2 迭代加密的次数
matcher.setHashIterations(3);
//3、将加密对象存储到myRealm中
myRealm.setCredentialsMatcher(matcher);
//4、将myRealm存入defaultWebSecurityManager对象
defaultWebSecurityManager.setRealm(myRealm);
//4.5 设置rememberMe
defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
//4.6、设置缓存
defaultWebSecurityManager.setSessionManager(sessionManager);
defaultWebSecurityManager.setCacheManager(redisCacheManager);
//5、返回
return defaultWebSecurityManager;
}
//shiro的sessionManager
@Bean
public DefaultWebSessionManager sessionManager(){
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
defaultWebSessionManager.setSessionDAO(redisSessionDAO);
return defaultWebSessionManager;
}
配置文件中指定redis的连接属性即可
shiro-redis:
enabled: true
redis-manager:
host: 127.0.0.1:6379
password: 123321
详细配置信息:
Title | Default | Description |
---|---|---|
shiro-redis.enabled | true |
Enables shiro-redis’s Spring module |
shiro-redis.redis-manager.deploy-mode | standalone |
Redis deploy mode. Options: standalone , sentinel , ‘cluster’ |
shiro-redis.redis-manager.host | 127.0.0.1:6379 |
Redis host. If you don’t specify host the default value is 127.0.0.1:6379 . If you run redis in sentinel mode or cluster mode, separate host names with comma, like 127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381 |
shiro-redis.redis-manager.master-name | mymaster |
Only used for sentinel mode The master node of Redis sentinel mode |
shiro-redis.redis-manager.timeout | 2000 |
Redis connect timeout. Timeout for jedis try to connect to redis server(In milliseconds) |
shiro-redis.redis-manager.so-timeout | 2000 |
Only used for sentinel mode or cluster mode The timeout for jedis try to read data from redis server |
shiro-redis.redis-manager.max-attempts | 3 |
Only used for cluster mode Max attempts to connect to server |
shiro-redis.redis-manager.password | Redis password | |
shiro-redis.redis-manager.database | 0 |
Redis database. Default value is 0 |
shiro-redis.redis-manager.count | 100 |
Scan count. Shiro-redis use Scan to get keys, so you can define the number of elements returned at every iteration. |
shiro-redis.session-dao.expire | -2 |
Redis cache key/value expire time. The expire time is in second. Special values: -1 : no expire -2 : the same timeout with session Default value: -2 Note: Make sure expire time is longer than session timeout. |
shiro-redis.session-dao.key-prefix | shiro:session: |
Custom your redis key prefix for session management Note: Remember to add colon at the end of prefix. |
shiro-redis.session-dao.session-in-memory-timeout | 1000 |
When we do signin, doReadSession(sessionId) will be called by shiro about 10 times. So shiro-redis save Session in ThreadLocal to remit this problem. sessionInMemoryTimeout is expiration of Session in ThreadLocal. Most of time, you don’t need to change it. |
shiro-redis.session-dao.session-in-memory-enabled | true |
Whether or not enable temporary save session in ThreadLocal |
shiro-redis.cache-manager.principal-id-field-name | id |
Principal id field name. The field which you can get unique id to identify this principal. For example, if you use UserInfo as Principal class, the id field maybe id , userId , email , etc. Remember to add getter to this id field. For example, getId() , getUserId( ), getEmail() , etc. Default value is id , that means your principal object must has a method called getId() |
shiro-redis.cache-manager.expire | 1800 |
Redis cache key/value expire time. The expire time is in second. |
shiro-redis.cache-manager.key-prefix | shiro:cache: |
Custom your redis key prefix for cache management Note: Remember to add colon at the end of prefix. |