Shiro
shiro是apache旗下一个Java安全框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证、权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。使用shiro就可以非常快速的完成认证、授权、加密,会话管理等功能的开发,可为任何应用提供安全保障,降低系统成本
Shiro功能
- 登录/身份认证,登录失败次数限制
- 对用户执行访问控制,如: 判断用户是否拥有角色admin,判断用户是否拥有访问的权限
- 在任何环境下使用Session API。例如CS程序
- 加密,保证数据安全;
- 可以使用多个用户数据源。例如一个是oracle用户库,另外一个是mysql用户库
- Caching:缓存(支持分布式cache管理);
- 单点登录SSO功能
- "Remember Me"服务,类似购物车的功能,shiro官方建议开启
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
- Shiro可以很友好的与Spring集成,支持jsp标签
Shiro三大核心组件
Shiro有三大核心的组件:Subject、SecurityManager 和 Realm
ApplicationCode为客户端,在Web环境中为登录的Controller,使用者只需要创建一个Subject对象,调用其上的login()方法,即可完成登录。在使用者角度只需要在SpringIOC容器中配置ShiroSecurityManager注入Realm即可简单使用
Subject认证主体,是需要认证的东西,最常见的就是用户名密码。它包含两个信息Principals和Credentials
- Principals身份。可以是用户名,邮件,手机号码等,用来标识一个登录主体身份
- Credentials凭证。常见有密码,数字证书等等。
SecurityManager安全管理员。这是Shiro架构的核心,它就像Shiro内部所有原件的保护伞一样。在项目中一般都会配置SecurityManager,开发人员大部分精力主要是在Subject认证主体上面。在与Subject进行交互的时候,实际上是SecurityManager在背后做一些安全操作
Realm是一个域,它是连接Shiro和具体应用的桥梁,当需要与安全数据交互的时候,比如用户账户、访问控制等,Shiro就会从一个或多个Realm中去查找。可以把Realm看成DataSource,即安全数据源,一般会自己定制Realm,在自定义Realm中一般会从数据库中获取和认证相关的信息
Spring Security和Shiro
在 Spring Boot 中做权限管理,一般来说,主流的方案是 Spring Security ,但是由于 Spring Security 过于庞大和复杂,只要能满足业务需要,大多数公司还是会选择 Apache Shiro 来使用。
- 适用范围: Spring Security 必须和 Spring MVC 一起使用, 而 Shiro 是一个通用的安全框架, 可以用在 Spring MVC 或 JavaEE/JavaSE 环境中
- 复杂程度: Spring Security 是一个非常复杂的架构, 虽然搭配 Spring Boot 后, 使用的难度小了些, 但是还是很复杂. Shiro 概念少, 使用也简单了很多
- 版本变动: Spring Security 变化很频繁, 网上教程多是基于 Spring Boot 1.x 的, 在 Spring Boot 2.x 下很多都不适用了. Shiro 从 2010 年发展到现在, 功能和代码已经相当成熟了, 几乎不会有大的变动
- 功能多少方面, 方面: Spring Security 内置支持 CSRF 保护, Shiro 没有开箱即用, 基于 Shiro 的项目 buji-pac4j 实现了 CSRF 保护.
Shiro过程
- 创建 SecurityManager 对象。对于 Web 项目可以直接new DefaultWebSecurityManager子类, ini 文件的realm也可以使用 IniSecurityManagerFactory 工厂方法创建
- 在程序中注入SecurityManager对象。对于Spring项目需要定义一个ShiroFilterFactoryBean,然后通过该bean来注册SecurityManager,shiroFilterFactoryBean.setSecurityManager(securityManager)。对于其他项目, 可以使用 SecurityUtils.setSecurityManager(securityManager) 注入
- 进行资源授权配置。对于 Spring 项目, 可以用 ShiroFilterFactoryBean bean来设置拦截器;也可以在 ini 文件的 url 节中定义资源授权配置
- 定义 Realm, 实现 doGetAuthenticationInfo(AuthenticationToken) 和 doGetAuthorizationInfo() 方法. doGetAuthenticationInfo( authcToken)。对于 JDBC , 一般是 user/role/permission 三张表, 以及 user_role/role_permssion 两个关系表,也可以在 Ini 文件中定义, 基本用作demo
- 使用 SecurityUtils.getSubject() 获取 subject, 然后调用 subject.login(), 该方法将触发 realm 的 doGetAuthenticationInfo(AuthenticationToken authcToken) 方法, 进行身份验证
- 在进行权限验证的时候, 会触发 realm 的 doGetAuthorizationInfo(PrincipalCollection principals) 函数, 由该函数进行角色和权限的验证
权限管理模型
RBAC模型
-
主体(账号、密码)
-
资源(资源名称、访问地址)
-
权限(权限名称、资源id)
-
角色(角色名称)
-
角色和权限关系(角色id、权限id)
-
主体和角色关系(主体id、角色id)
通常企业开发中将资源和权限表合并为一张权限表,包括资源(资源名称、访问地址)和权限(权限名称、资源id)。合并为:权限(权限名称、资源名称、资源访问地址)
基于角色的访问控制是不利于系统维护,可扩展性不强。
角色针对人划分的,人作为用户在系统中属于活动内容,如果该角色可以访问的资源出现变更,需要修改代码了
资源在系统中是不变的,比如资源有:类中的方法,页面中的按钮。一般建议使用基于资源的访问控制实现权限管理
口令的加密
口令是用于验证,证明你是你说的人
- 口令采用md5加密,为了避免使用碰撞算法进行解密,所以引入盐值和多次进行hash计算
盐值的生成
uuid生成32位长的随机串
String ss = UUID.randomUUID().toString();
ss = ss.replaceAll("-", "");
System.out.println(ss+"----"+ss.length());
使用shiro提供的随机算法生成32位长的字符串
String salt2 = new SecureRandomNumberGenerator().nextBytes().toHex();
System.out.println(salt2+"-----"+salt2.length());
口令加密
只使用md5加密
String pwd="123456";
String salt="有可能会使用用户名充当盐值";
int num=3;//进行hash计算的次数,次数值越大安全性越高,但是cpu算力的浪费越明显
//构造方法中:参数1明文,原始密码; 参数2盐,通过使用随机数; 参数3散列的次数,比如散列两次,相当于md5(md5(''))
Md5Hash md5=new Md5Hash(pwd,salt,num);
String res=md5.toHex(); //32位长度 9c156fae1410d97592e764fa4526e54e
System.out.println(res+"----"+res.length());
res=md5.toBase64(); //24位长度 nBVvrhQQ2XWS52T6RSblTg==
System.out.println(res+"----"+res.length());
可以在md5和sha-1之间进行选择
String pwd="123456";
String tring salt="有可能会使用用户名充当盐值";
int num=3;
SimpleHash sh=new SimpleHash("sha-1",pwd,salt,num);
System.out.println(sh.toHex());
当使用md5针对口令进行加密后,则不能支持取回口令操作。如果需要提供取回口令操作,则应该是使用固定值进行覆盖
例如
password=123456
salt=qianduanaihuangmao
result=2c6037e1c8cf47036f8d9b55da3dfa09
具体开发
用户表—记录用户的登录信息
create table if not exists t_users(
id bigint primary key auto_increment,
username varchar(20)not null unique,
password varchar(32)not null,
salt varchar(32) not null
)engine=innodb default charset utf8;
角色表—一组权限的别名
create table if not exists t_roles(
id bigint primary key auto_increment,
name varchar(32) not null unique
)engine=innodb default charset utf8;
权限表—限制资源访问
create table if not exists t_perms(
id bigint primary key auto_increment,
title varchar(32) not null,
perm_str varchar(50) comment '权限串,一般命名格式为users:select:*',
url varchar(50)
)engine=innodb default charset utf8;
用户和角色之间是一个多对多的关联关系
create table if not exists t_roles_users(
role_id bigint not null,
user_id bigint not null,
primary key(role_id,user_id),
foreign key(role_id) references t_roles(id) on delete cascade,
foreign key(user_id) references t_users(id) on delete cascade
)engine=innodb default charset utf8;
角色和权限之间是一个多对多的关联关系
create table if not exists t_roles_perms(
role_id bigint not null,
perm_id bigint not null,
primary key(role_id,perm_id),
foreign key(role_id) references t_roles(id) on delete cascade,
foreign key(perm_id) references t_perms(id) on delete cascade
)engine=innodb default charset utf8;
添加依赖完成持久层开发
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
核心配置
# 数据库驱动:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 数据源名称
spring.datasource.name=defaultDataSource
# 数据库连接地址
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC
# 数据库用户名&密码:
spring.datasource.username=root
spring.datasource.password=123456
主类上添加自动扫描
@MapperScan("com.yan.dao")
@SpringBootApplication
public class Shiro2Application {
定义实体类
@Data
@TableName("t_users")
public class User implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String salt;
}
定义对应的映射接口
@Repository
public interface UserMapper extends BaseMapper<User> {
}
定义业务接口
public interface IUserServ extends IService<User> {
}
定义业务接口的实现类
@Transactional(readOnly = true,propagation = Propagation.SUPPORTS)
@Service
public class UserServImpl extends ServiceImpl<UserMapper, User> implements IUserServ {
}
同步&异步
针对同步处理的控制器定义
登录验证流程:
创建SecurityManager安全管理器 > 主体Subject提交认证信息 > SecurityManager安全管理器认证 > SecurityManager调用Authenticator认证器认证 >Realm验证
shiro过滤器过滤属性含义
securityManager:这个属性是必须的。
loginUrl :没有登录的用户请求需要登录的页面时自动跳转到登录页面,不是必须的属性,不输入地址的话会自动寻找项目web项目的根目录下的”/login.jsp”页面。
successUrl :登录成功默认跳转页面,不配置则跳转至”/”。如果登陆前点击的一个需要登录的页面,则在登录自动跳转到那个需要登录的页面。不跳转到此。
unauthorizedUrl :没有权限默认跳转的页面
授权操作一般常见方法有添加注解或者使用页面的标签
权限控制注解
- @RequiresGuest 只有游客可以访问
- @RequiresAuthentication 需要登录才能访问
- @RequiresUser 已登录的用户或“记住我”的用户能访问
- @RequiresRoles 已登录的用户需具有指定的角色才能访问
- @RequiresPermissions 已登录的用户需具有指定的权限才能访问
@RequireXXX注解可能的Bug
注意:解决spring aop和注解配置一起使用的bug。如果您在使用shiro注解配置的同时,引入了spring aop的starter,会有一个奇怪的问题,导致shiro注解的请求,不能被映射,需加入以下配置:
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
/** setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。 加入这项配置能解决这个bug */
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
//defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
在控制器上添加权限注解
@RequiresRoles("admin")
@RequiresPermissions("users:add")
@RequestMapping(value ={"/","/show"},method = {RequestMethod.GET,RequestMethod.POST})
public String list(Model model)throws Exception{
List<User> userList= userService.list();
model.addAttribute("userList",userList);
return "user/show";
}
在页面上进行权限控制
<!--thymeleaf 对shiro的扩展支持-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
标签使用
<html lang = "en" xmlns:th = "http://www.thymeleaf.org" xmlns:shiro = "http://www.pollix.at/thymeleaf/shiro" >
<!-- 显示登陆用户昵称 -->
<shiro:principal/>
<!-- 游客 -->
<p shiro:guest = "" > Please <a href = "login.html" > login </a></p>
<!-- 认证通过或已记住的用户。-->
<p shiro:user = "" >
Welcome back John! Not John? Click <a href = "login.html" > here </a> to login.
</p>
<!-- 已认证通过的用户。不包含已记住的用户,这是与user标签的区别所在。-->
<p shiro:authenticated = "" >
Hello, <span shiro:principal = "" ></span> , how are you today?
</p>
<!-- 验证当前用户是否属于该角色。-->
<a shiro:hasRole = "admin" href = "admin.html" > Administer the system </a> <!-- 拥有该角色 -->
<!-- 验证当前用户是否拥有以下任意一个权限。-->
<p shiro:hasAnyPermissions = "userInfo:view, userInfo:del" > <!-- 权限或判断 -->
You can see or delete users.
</p>
<div shiro:hasPermission="user:add">
异步通讯的登录问题
JWT token
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。
该信息可以被验证和信任,因为它是数字签名的。
JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
1、JWT的构成
第一部分称它为头部header,第二部分称其为载荷payload,第三部分是签证signature
1.1、header
jwt的头部承载两部分信息:
- 声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HMAC SHA256
2.playload
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含
三个部分:1、标准中注册的声明。2、公共的声明。3、私有的声明
标准中注册的声明 (建议但不强制使用) :
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的
必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为
base64是对称解密的,意味着该部分信息可以归类为明文信息。
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:1、header (base64后的)
2、payload (base64后的)3、secret
secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行
jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。
一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
JSON Web Tokens是如何工作的
在认证的时候,当用户用他们的凭证成功登录以后,一个JSON Web Token将会被返回。此后,token就是用户凭证了,你必须非常小心以防止出现安全问题。一般而言,你保存令牌的时候不应该超过你所需要它的时间。
无论何时用户想要访问受保护的路由或者资源的时候,用户代理(通常是浏览器)都应该带上JWT,典型的,通常放在Authorization header中,用Bearer schema。
header应该看起来是这样的:
Authorization: Bearer
服务器上的受保护的路由将会检查Authorization header中的JWT是否有效,如果有效,则用户可以访问受保护的资源。如果JWT包含足够多的必需的数据,那么就可以减少对某些操作的数据库查询的需要,尽管可能并不总是如此。
如果token是在授权头(Authorization header)中发送的,那么跨源资源共享(CORS)将不会成为问题,因为它不使用cookie。
具体使用
依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
需要写token的生成方法
public String getToken(User user) {
String token= JWT.create().withAudience(user.getId())
.sign(Algorithm.HMAC256(user.getPassword()));
return token;
}
Algorithm.HMAC256():使用HS256生成token,密钥则是用户的密码,唯一密钥的话可以保存在服务端。
withAudience()存入需要保存在token的信息,这里我把用户ID存入token中
验证处理
String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
//从token中获取生成时对应的数据
String userId = JWT.decode(token).getAudience().get(0);
//调用业务方法按照用户id查询用户信息
User user = userService.findUserById(userId);
// 验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
//针对接收到的token进行验证,如果错误则异常
jwtVerifier.verify(token);
Shiro面试题
shiro的组成
认证Authenticator、授权Authorizer
session管理SessionManager
CacheManager即缓存管理
Cryptography即密码管理
比较 SpringSecurity 和 Shiro
Spring Security, Shiro 在保持强大功能的同时, 使用简单性和灵活性
SpringSecurity: 即使是一个一个简单的请求,最少得经过它的 8 个Filter
SpringSecurity 必须在 Spring 的环境下使用
初学 Spring Security, 曲线还是较大, 需要深入学习其源码和框架, 配置起来也较费力.
Shiro认证过程
①. 应用程序代码调用 Subject.login 方法,传递创建好的包含终端用户的 Principals(身份)和 Credentials(凭证)的 AuthenticationToken 实例
②. Subject 实例委托应用程序的 SecurityManager 通过调用securityManager.login(token) 开始真正的验证。
Subject 实例(通常为 DelegatingSubject或它的子类)
③. SubjectManager 接收 token,调用内部的 Authenticator 实例调用 authenticator.authenticate(token).Authenticator 通常是一个 ModularRealmAuthenticator 实例, 支持在身份验证中协调一个或多个Realm 实例
④. 如果应用程序中配置了一个以上的 Realm, ModularRealmAuthenticator 实例将利用配置好的AuthenticationStrategy 来启动 Multi-Realm 认证尝试。在Realms 被身份验证调用之前、调用期间、调用之后,AuthenticationStrategy 被调用使其能够对每个Realm 的结果作出反应。(AuthenticationStrategy都会被调用,对每个Realm 的结果作出反应)
⑤. 每个配置的 Realm 用来帮助看它是否支持提交的 AuthenticationToken. 如果支持, 那么支持 Realm 的 getAuthenticationInfo 方法将会伴随着提交的 token 被调用. getAuthenticationInfo 方法有效地代表一个特定 Realm 的单一的身份验证尝试。
Shiro授权过程
①. 应用程序或框架代码调用任何 Subject 的hasRole*, checkRole*, isPermitted*,或者checkPermission*方法的变体, 传递任何所需的权限
②. Subject 的实例 调用securityManager 的对应的方法.
Subject 实例(通常为 DelegatingSubject或它的子类)
③. SecurityManager 调用 org.apache.shiro.authz.Authorizer 接口的对应方法.默认情况下,authorizer 实例是一个 ModularRealmAuthorizer 实例, 它支持协调任何授权操作过程中的一个或多个Realm 实例
④. 每个配置好的 Realm 被检查是否实现了相同的 Authorizer 接口. 如果是, Realm 各自的hasRole*, checkRole*,isPermitted*,或 checkPermission*方法将被调用。
shiro权限认证的三种方式
1.使用URL实现权限控制
spring配置shirofiter配置url验证规则
2.使用注解实现权限控制
@RequiresPermissions({“ceshi.test”})
@RequiresRoles(“adminRole”)
3.使用shiro标签进行权限控制
异步的权限控制
JWT
Apache Shiro是一个轻量级的Java安全框架,提供身份认证、授权、加密和会话管理功能。它易于使用,可与Spring集成,适用于多种环境。Shiro的核心组件包括Subject、SecurityManager和Realm。相比Spring Security,Shiro更简单,适合不需要过于复杂安全需求的项目。Shiro通过Realm连接应用数据源,实现登录认证和权限授权。JWT在Shiro中可用于异步通讯的登录验证,提供安全的身份验证。
986

被折叠的 条评论
为什么被折叠?



