Shiro(2)

Apache Shiro是一个轻量级的Java安全框架,提供身份认证、授权、加密和会话管理功能。它易于使用,可与Spring集成,适用于多种环境。Shiro的核心组件包括Subject、SecurityManager和Realm。相比Spring Security,Shiro更简单,适合不需要过于复杂安全需求的项目。Shiro通过Realm连接应用数据源,实现登录认证和权限授权。JWT在Shiro中可用于异步通讯的登录验证,提供安全的身份验证。

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 来使用。

  1. 适用范围: Spring Security 必须和 Spring MVC 一起使用, 而 Shiro 是一个通用的安全框架, 可以用在 Spring MVC 或 JavaEE/JavaSE 环境中
  2. 复杂程度: Spring Security 是一个非常复杂的架构, 虽然搭配 Spring Boot 后, 使用的难度小了些, 但是还是很复杂. Shiro 概念少, 使用也简单了很多
  3. 版本变动: Spring Security 变化很频繁, 网上教程多是基于 Spring Boot 1.x 的, 在 Spring Boot 2.x 下很多都不适用了. Shiro 从 2010 年发展到现在, 功能和代码已经相当成熟了, 几乎不会有大的变动
  4. 功能多少方面, 方面: Spring Security 内置支持 CSRF 保护, Shiro 没有开箱即用, 基于 Shiro 的项目 buji-pac4j 实现了 CSRF 保护.

Shiro过程

  1. 创建 SecurityManager 对象。对于 Web 项目可以直接new DefaultWebSecurityManager子类, ini 文件的realm也可以使用 IniSecurityManagerFactory 工厂方法创建
  2. 在程序中注入SecurityManager对象。对于Spring项目需要定义一个ShiroFilterFactoryBean,然后通过该bean来注册SecurityManager,shiroFilterFactoryBean.setSecurityManager(securityManager)。对于其他项目, 可以使用 SecurityUtils.setSecurityManager(securityManager) 注入
  3. 进行资源授权配置。对于 Spring 项目, 可以用 ShiroFilterFactoryBean bean来设置拦截器;也可以在 ini 文件的 url 节中定义资源授权配置
  4. 定义 Realm, 实现 doGetAuthenticationInfo(AuthenticationToken) 和 doGetAuthorizationInfo() 方法. doGetAuthenticationInfo( authcToken)。对于 JDBC , 一般是 user/role/permission 三张表, 以及 user_role/role_permssion 两个关系表,也可以在 Ini 文件中定义, 基本用作demo
  5. 使用 SecurityUtils.getSubject() 获取 subject, 然后调用 subject.login(), 该方法将触发 realm 的 doGetAuthenticationInfo(AuthenticationToken authcToken) 方法, 进行身份验证
  6. 在进行权限验证的时候, 会触发 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值