579 lines
29 KiB
Markdown
579 lines
29 KiB
Markdown
---
|
||
title: 使用Shiro替换Spring Security
|
||
tags:
|
||
- Java
|
||
- Spring
|
||
- Spring Security
|
||
- Shiro
|
||
- Spring MVC
|
||
- 安全认证
|
||
categories:
|
||
- - JVM
|
||
- Spring
|
||
keywords: 'Spring,Spring Security,Shiro,Realm,Subject,Authentication,Authorization'
|
||
date: 2021-09-29 17:57:29
|
||
---
|
||
|
||
之前用了几篇文章分析和记录了Spring Security的用法,但是相比其他安全框架来说,Spring Security虽然与Spring Framework结合紧密,却无法称得上轻量。所以在很多项目中,Spring Security就被很自然的放在一边了,转而采用其他的安全框架。Shiro就是这样一个轻量级的选择。<!-- more -->
|
||
|
||
Shiro是一款不依赖于任何容器的,可以通用于Java SE和Jakarta EE的安全框架。根据官网文档的描述,SHiro设计有四大功能:Authentication(认证)、Authorization(授权)、Session Management(会话管理)、Cryptography(密码学)。在将Shiro应用到用户安全管理中,最常用的就是使用其Authentication和Authorization两个功能。
|
||
|
||
这片文章将会对Shiro的基础概念和使用进行一个简单的分析,并最后给出一个能够对用户进行鉴权并提供用户使用Token操作的示例。
|
||
|
||
## 基础概念
|
||
|
||
跟Spring Security中一样,Shiro也提供了一些设施来完成用户鉴权的功能。这些常用的功能设施主要有以下这些。
|
||
|
||
1. `UsernamePasswordToken`,用于保存用户录入的用户名和密码,之后交由Shiro进行认证。
|
||
1. `BearerToken`,用于保存用户提供的令牌,之后也同样交由Shiro进行认证。
|
||
1. `Subject`,Shiro抽象出来的用于包含用户信息的对象。
|
||
1. `SecurityManager`,用于提供认证与授权功能。
|
||
1. `Realm`,由用户自行定义的用于执行鉴权和授权逻辑的实现模块,并对用户的访问进行控制。
|
||
1. `AuthenticationInfo`,用户的角色信息集合,主要用于认证。
|
||
1. `AuthorizationInfo`,用户的权限信息集合,主要用于授权。
|
||
1. `SecurityManager`,Shiro的安全管理器,用于管理所有系统中存在的`Subject`。
|
||
1. `ShiroFilterFactoryBean`,Shiro的过滤器工厂,是由用户自定义的处理流水线。用户定义的处理流程就是由一个个的`Filter`组成的。
|
||
1. `SecurityUtils`,Shiro提供的一个用于常用操作的工具类,其中集成了常用功能设施的创建和获取操作。
|
||
|
||
在项目中使用Shiro的时候,一般也是通过定义这些内容来完成用户的认证和授权的过程的。在开始使用Shiro之前,还是照旧先看一下以上每种设施的类继承结构,方便对Shiro的整体架构有一个初步的了解。
|
||
|
||
### AuthenticationToken
|
||
|
||
Token是一个用于承载用于用户认证信息的容器,常用的Token只有`UsernamePasswordToken`和`BearerToken`两个,但是支撑它们的还有三个接口,如下图所示。
|
||
|
||
{% oss_image shiro-spring/token-structure.svg AuthenticationToken类继承结构 450 %}
|
||
|
||
所有的Token都间接实现了`AuthenticationToken`接口,而`AuthenticationToken`中定义的`getPrincipal()`则是用于获取用户信息的,`getCredentials()`则是用来获取用户密钥之类的认证信息的。这就为如何使用`AuthenticationToken`指明了一条路线。
|
||
|
||
### Subject
|
||
|
||
`Subject`要比`AuthenticationToken`保存的内容要更多,但增加的内容主要是用户是否经过了认证和授权,以及用户所具有的权限信息。在整个项目中,对于用户的认证与授权信息,都是通过获取`Subject`实例来得到的。所以`Subject`也是在整个项目中获取当前登录用户信息的首要途径。`Subject`接口中的内容比较多,但是并不复杂,而且`Subject`接口相关的类也相对比较少,主要相关的类之间的关系如下图所示。
|
||
|
||
{% oss_image shiro-spring/subject-structure.svg Subject类的相关结构 1350 %}
|
||
|
||
`Subject`在整个项目中通常代表一个用户,不管这个用户是已经登录成功了,还是被拒绝访问了,都会在系统中存在一个对应的Subject。而且从上面的结构图上也可以看到,`Subject`基本上是Shiro在处理用户登录与授权时的一个核心类。
|
||
|
||
### SecurityManager
|
||
|
||
`SecurityManager`在整个系统中启动并执行了所有针对于`Subject`的安全操作。但是在实际的代码编写中,我们并不会直接去调用`securityManager`的实例来完成什么操作,一般都是使用`SecurityUtils`工具类获取一个`Subject`实例来进行一系列的操作的。但是对于`SecurityMananger`的结构还是需要了解一下的。
|
||
|
||
{% oss_image shiro-spring/securitymanager-structure.svg SecurityManager类的相关结构 1350 %}
|
||
|
||
经过图中所示的层层继承和实现,在项目应用的通常都是`DefaultSecurityManager`的实例,它的功能已经足够满足日常项目所需。在使用的时候,只需要使用其中的`setRealm()`方法将自定义的`Realm`放置进去即可。
|
||
|
||
### Authenticator
|
||
|
||
`Authenticator`是用来对用户提交的`AuthenticationToken`进行认证的,`AuthenticationToken`经过`Authenticator`处理以后,会返回一个`AuthenticationInfo`实例,其中包括了已经完成认证的用户的所有账户信息。此外,`Authenticator`还通过`AuthenticationStrategy`控制着项目中有多个`Realm`的时候如何完成用户认证操作的策略。
|
||
|
||
常用的`Authenticator`相关的类结构,可以参考下图。
|
||
|
||
{% oss_image shiro-spring/authenticator-structure.svg Authenticator类的相关结构 850 %}
|
||
|
||
在配置项目中所使用的`SecurityManager`的时候,一般并不需要手工实例化`Authenticator`,只需要通过`SecurityManager`提供的`getAuthenticator()`方法获取其中持有的`Authenticator`实例,然后再配置其中所需的`AuthenticationStrategy`即可。这里需要注意的是,如果整个项目中只使用了一个`Realm`,那么就无需配置`Authenticator`的`AuthenticationStrategy`。
|
||
|
||
### Realm
|
||
|
||
`Realm`是一个由用户自定义的专门用于确定用户身份和授权信息的实体。`Realm`通常可以访问项目中的所有安全相关信息,包括用户信息、权限信息等,在整个项目中通常都扮演一个特殊的DAO角色。在Shiro的设计中,每一个`Realm`都只实现一种认证功能,`Realm`可以根据所传入的`AuthenticationToken`的具体类型来确定自身是否能对给定的认证信息进行认证;而`Authenticator`也将根据所设置的`AuthenticationStrategy`确定多个`Realm`之间的组合认证方式。
|
||
|
||
在Shiro中`Realm`只是一个非常简练的接口,在实际使用中一般不会直接去实现这个接口,而是会选择继承其已有的实现。常用的有关`Realm`的类结构,可以参考下图。
|
||
|
||
{% oss_image shiro-spring/realm-structure.svg Realm类的相关结构 1350 %}
|
||
|
||
根据这张`Realm`类的结构图,可以看出,如果需要自定义`Realm`实现,一般可以直接继承`AuthorizingRealm`类,这个类中已经继承并实现了大部分的用户验证逻辑,可以直接复用。如果定义了多个`Realm`,那么可以通过`Realm`中的`support()`方法来决定自身所能够处理的`AuthenticationToken`类型。
|
||
|
||
### SecurityUtils
|
||
|
||
如果在其他的网站上看过Shiro的教程,那么一般会发现`SecurityUtils`这个工具类出现的次数十分频繁。这是因为这个工具类通常是被用来创建`Subject`以及获取当前项目中的`SecurityManager`的。使用`SecurityUtils`而不是手动创建`Subject`这些类,可以确保类实例的从属关系。在`SecurityUtils`中,主要的方法有以下两个。
|
||
|
||
1. `getSubject()`,从当前的`ThreadContext`中获取一个`Subject`实例或者创建一个新的`Subject`实例。
|
||
1. `getSecurityManager()`,从当前的`ThreadContext`中获取一个`SecurityManager`实例。
|
||
|
||
在后面的整合示例中,将会看到`SecurityUtils`的使用方式。但是有一点是需要注意的,如果需要使用`Securityutils`中的功能,必须先使用`setSecurityManager()`方法为Shiro设置一个全局单例的`SecurityManager`实例。
|
||
|
||
### 过滤器
|
||
|
||
将Shiro的认证与授权处理流程融入进Web项目中也同样是依靠Servlet Filter的。过滤器在Shiro的处理流程里可以承担对请求进行预处理的工作,并且可以对请求中提供的令牌、密钥等进行认证与授权的操作。所以,过滤器也是Shiro处理流程中一个主要的生成`AuthenticationToken`的起点。Shiro中的过滤器也是一个比较庞大的体系,如果在项目中需要自己实现一个过滤器,那么可以参考下图来选择所要继承的类。
|
||
|
||
{% oss_image shiro-spring/shiro-filter-structure.svg "Shiro过滤器类的相关结构" 1350 %}
|
||
|
||
通常我们都需要在上面这张图中,选择比较靠上的类来继承,一般会选择`BasicHttpAuthenticationFilter`。这样可以利用`createToken()`方法来创建用于代表用户登录信息的`AuthenticationToken`,再利用`AuthenticatingFilter`中提供的`executeLogin()`方法将自动生成的`AuthenticationToken`登入进来。
|
||
|
||
## 与Spring整合
|
||
|
||
既然要使用Shiro替代Spring Security来完成用户认证与授权功能,那么就要在Spring框架下构建和组装Shiro完成认证所需要用到的类实例。在目前的Spring框架开发过程中,都是使用Spring Boot脚手架框架来支持Spring应用的快速建立和配置。所以在构建使用Shiro进行用户认证和授权的应用时,也需要先在项目中加入用于自动化配置Shiro的Starter。
|
||
|
||
以下是Shiro-Spring Starter的GAV坐标。
|
||
|
||
```xml
|
||
<dependency>
|
||
<groupId>org.apache.shiro</groupId>
|
||
<artifactId>shiro-spring-boot-starter</artifactId>
|
||
<version>${shiro.spring.version}</version>
|
||
</dependency>
|
||
```
|
||
|
||
!!! info ""
|
||
在本文进行编写的时候,`shiro-spring-boot-starter`的版本是1.8.0。你可能在Shiro的官方网站上看到了Shiro与Spring整合的说明,但是请注意,那片说明是针对Shiro与Spring应用整合的,而不是用于Spring Boot应用的。另外,在网上许多教程与介绍中,都建议使用`shiro-spring-boot-web-starter`,但是通过对Shiro源码的查看,`shiro-spring-boot-web-starter`已经合并进了`shiro-spring-boot-starter`,所以直接选用最短的那个就好。
|
||
|
||
### 认证与授权功能流程
|
||
|
||
在开始使用Shiro进行用户认证功能的编写之前,需要先对Shiro结合到Web项目中以后的整个工作流程有一个大概的了解。下图从一个Web请求开始,绘制了用户认证信息与授权信息的形成和流转流程。
|
||
|
||
{% oss_image shiro-spring/shiro-authc-flow.svg Shiro认证与授权概要流程 550 %}
|
||
|
||
在这个过程中,实际上有许多过程是不需要我们去实现的,我们所需要实现的内容主要是对于用户信息的获取、`AuthenticationToken`与`AuthenticationInfo`的组织等。当然在Controller中对用户提供的用户名和密码等认证信息的鉴别还是需要我们编码来执行的。
|
||
|
||
!!! info ""
|
||
其实对于用户认证处理最核心的就是,只要调用`subject.login(token)`时不抛出任何异常,那么用户就已经被系统认证通过了,可以在之后的操作中通过获取`Subject`实例来得到用户的相关信息。
|
||
|
||
!!! caution ""
|
||
需要注意的是,`shiro-spring-boot-starter`中对于Bean的引用大部分都是通过Bean名称的,所以如果在运行Spring应用的时候,提示缺少某一个名称的Bean,那么可以检查一下应用中的Shiro配置,看看是不是已经定义了相应类型的Bean,但是没有使用Shiro需求的名字。这样的话可以直接给应用中的Bean重新起一个名字即可。
|
||
|
||
### Shiro配置
|
||
|
||
对Shiro的配置,主要是配置Shiro的处理流程,通过定义其中的关键部件,使上图中的流程完全打通。在加入其他的功能类之前,可以首先先在项目中添加一个仅有基本功能的配置类。
|
||
|
||
```java
|
||
@Configuration
|
||
@Import({
|
||
ShiroBeanConfiguration.class,
|
||
ShiroAnnotationProcessorConfiguration.class,
|
||
ShiroWebConfiguration.class,
|
||
ShiroWebFilterConfiguration.class,
|
||
ShiroRequestMappingConfig.class
|
||
})
|
||
public class WebShiroConfig {
|
||
private final SecurityManager securityManager;
|
||
|
||
public WebShiroConfig(SecurityManager securityManager) {
|
||
this.securityManager = securityManager;
|
||
SecurityUtils.setSecurityManager(this.securityManager);
|
||
}
|
||
|
||
@Bean
|
||
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
|
||
var chainDefinition = new DefaultShiroFilterChainDefinition();
|
||
// 将所有路径都设置为允许匿名访问,可以使Shiro的注解控制生效
|
||
chainDefinition.addPathDefinition("/**", "anon");
|
||
// 或者可以全部允许访问的方式来使Shiro注解控制生效
|
||
// 两种方法选择其中的一种即可
|
||
chainDefinition.addPathDefinition("/**", "authcBasic[permissive]");
|
||
|
||
// 当然如果不使用Shiro的注解控制,就可以在这里编写根据路径的权限控制,例如:
|
||
// 设置用户必须登入系统,并且拥有admin角色
|
||
chainDefinition.addPathDefinition("/admin/**", "authc, roles[admin]");
|
||
// 设置用户必须拥有stroage:write权限
|
||
chainDefinition.addPathDefinition("/store/**", "perms[storage:write]");
|
||
// 设置用户必须登入系统
|
||
chainDifinition.addPathDefinition("/api/**", "authc");
|
||
|
||
return chainDefinition;
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 关闭Session
|
||
|
||
关闭Session的支持可以让Shiro以Stateless的方式接入Spring MVC中。要关闭Session,需要从`SubjectFactory`中的`createSubject()`方法进行设置,这就需要重写一套`SubjectFactory`,但是这个类并不麻烦,只是需要记得将其注入到Shiro的设置中。
|
||
|
||
```java
|
||
@Component
|
||
@Order(1)
|
||
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
|
||
@Override
|
||
public Subject createSubject(SubjectContext context) {
|
||
// 设置SubjectContext,不为Subject创建Session
|
||
context.setSessionCreationEnabled(false);
|
||
|
||
return super.createSubject(context);
|
||
}
|
||
}
|
||
```
|
||
|
||
另外还需要在上面的`WebShiroConfig`类中增加以下几个Bean,以关闭Session相关的其他设施。
|
||
|
||
```java
|
||
@Bean
|
||
@Order(1)
|
||
public SessionManager sessionManager() {
|
||
var sessionManager = new DefaultSessionManager();
|
||
// 关闭Session验证
|
||
sessionManager.setSessionValidationSchedulerEnabled(false);
|
||
return sessionManager;
|
||
}
|
||
|
||
@Bean
|
||
@Order(1)
|
||
public SessionStorageEvaluator sessionStorageEvaluator() {
|
||
var evaluator = new DefaultSessionStorageEvaluator();
|
||
// 关闭Session存储
|
||
evaluator.setSessionStorageEnabled(false);
|
||
return evaluator;
|
||
}
|
||
```
|
||
|
||
!!! caution ""
|
||
Shiro的Spring Boot Starter中大部分Bean都使用了`@ConditionOnMissingBean`的注解,所以如果需要覆盖某一个Bean,只需要直接定义自己的Bean即可。
|
||
|
||
因为Shiro的Spring Boot Starter中没有采用依赖注入的方式创建`SubjectDAO`、`SecurityManager`等实例,所以这些必须的实例也需要在配置类中手动创建。
|
||
|
||
```java
|
||
@Bean
|
||
public SubjectDAO subjectDAO(SessionStorageEvaluator evaluator) {
|
||
var dao = new DefaultSubjectDAO();
|
||
dao.setSessionStorageEvaluator(evaluator);
|
||
return dao;
|
||
}
|
||
|
||
@Bean
|
||
public ModularRealmAuthenticator modularRealmAuthenticator() {
|
||
var authenticator = new ModularRealmAuthenticator();
|
||
authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
|
||
return authenticator;
|
||
}
|
||
|
||
// 这里先加一个剧透,把后面要用的两个用于认证的Realm加入进来供SecurityManager使用。
|
||
@Bean(name = "realms")
|
||
public List<Realm> authenticationRealms() {
|
||
List<Realm> realms = new LinkedList<>();
|
||
realms.add(new UsernameRealm());
|
||
realms.add(new TokenRealm());
|
||
return realms;
|
||
}
|
||
```
|
||
|
||
之后建立一个SecurityManager的实例来组织认证所需要的内容。
|
||
|
||
```java
|
||
@Component
|
||
public class MultiRealmsSecurityManager extends DefaultWebSecurityManager {
|
||
|
||
public MultiRealmsSecurityManager(
|
||
SubjectDAO subjectDAO,
|
||
SubjectFactory subjectFactory,
|
||
RememberMeManager rememberMeManager,
|
||
ModularRealmAuthenticator authenticator,
|
||
@Qualifier("realms") List<Realm> realmList
|
||
) {
|
||
this.setSubjectDAO(subjectDAO);
|
||
this.setSubjectFactory(subjectFactory);
|
||
if (Objects.nonNull(rememberMeManager)) {
|
||
this.setRememberMeManager(rememberMeManager);
|
||
}
|
||
this.setAuthenticator(authenticator);
|
||
super(realmList);
|
||
}
|
||
|
||
}
|
||
```
|
||
|
||
!!! info ""
|
||
因为所构建的应用是一个提供Web服务的应用,所以需要继承`DefaultWebSecurityManager`来提供对于Web的安全控制。
|
||
|
||
#### 构建对用户名与密码的验证
|
||
|
||
首先假设应用中的用户表结构是如下面这样定义的。
|
||
|
||
```java
|
||
@Getter
|
||
@Setter
|
||
@Builder
|
||
@NoArgsConstructor
|
||
@RequiredArgsConstructor
|
||
@Entity
|
||
public class Member {
|
||
@Id
|
||
@NonNull
|
||
private Long id;
|
||
|
||
@Column(length = 50, nullable = false)
|
||
@NonNull
|
||
private String username;
|
||
|
||
@Column(length = 150, nullable = false)
|
||
@NonNull
|
||
private String password;
|
||
|
||
@Column(length = 70)
|
||
private String fullName;
|
||
|
||
@Column(nullable = false)
|
||
@NonNull
|
||
private boolean enabled;
|
||
|
||
@ManyToMany(cascade = CascadeType.ALL)
|
||
@JoinTable(
|
||
name = "member_roles",
|
||
joinColumns = {@JoinColumn(name = "mid")},
|
||
inverseJoinColumns = {@JoinColumn(name = "rl_keyword")}
|
||
)
|
||
private Set<Role> roles;
|
||
}
|
||
```
|
||
|
||
因为假定Java应用采用Spring Data JPA对数据库访问进行包装,这样应用中就会存在一个名为`MemberRepository`的接口来进行数据库的访问操作。
|
||
|
||
对于用于在Shiro中承载待验证信息的`AuthenticationToken`类,可以直接使用Shiro内置的`UsernamePasswordToken`类,也可以选择自行实现一个具有特定功能的`AuthenticationToken`类。在这个示例中,将直接使用`UsernamePasswordToken`类。有了这两个支持类,就可以定义一个专门用于验证用户名和密码的`Realm`类了。
|
||
|
||
```java
|
||
@Component
|
||
@RequiredArgsConstructor
|
||
public class UsernameRealm extends AuthoringRealm {
|
||
private final MemberRepository memberRepository;
|
||
|
||
@PostConstruct
|
||
private void initRealm() {
|
||
super.setCredentialsMatcher(new HashedCredentialsMatcher("SHA-512"));
|
||
}
|
||
|
||
@Override
|
||
public boolean supports(AuthenticationToken token) {
|
||
return token instanceof UsernamePasswordToken;
|
||
}
|
||
|
||
@Override
|
||
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
|
||
var username = (String) principalCollection.getPrimaryPrincipal();
|
||
var roles = memberRepository.findMemberByUsername(username)
|
||
.stream()
|
||
.map(Member::getRoles)
|
||
.flatMap(Collection::stream)
|
||
.map(Role::getKeyword)
|
||
.collect(Collectors.toSet());
|
||
return new SimpleAuthorizationInfo(roles);
|
||
}
|
||
|
||
@Override
|
||
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
|
||
var token = (UsernamePasswordToken) authenticationToken;
|
||
return memberRepository.findMemberByUsername((String) token.getPrincipal())
|
||
.map(member -> new SimpleAuthenticationInfo(member.getUsername(), member.getPassword(), "UsernameRealm"))
|
||
.orElseThrow(() -> new AuthenticationException("用户不存在。"));
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 构建对用户令牌的验证
|
||
|
||
如果要支持用户令牌的鉴权方式,那么就需要在应用中构建一个用于鉴别用户令牌真伪的机制,在这个示例中将采用将已经颁发的用户令牌保存到数据库的方式。与用户信息一样,这种方式也需要一个数据库实体类。
|
||
|
||
```java
|
||
@Getter
|
||
@Setter
|
||
@Builder
|
||
@NoArgsConstructor
|
||
@RequiredArgsConstructor
|
||
@Entity
|
||
public class TokenStore {
|
||
@Id
|
||
@NonNull
|
||
private String token;
|
||
|
||
@NonNull
|
||
@Column(nullable = false)
|
||
LocalDateTime expiresAt;
|
||
|
||
@OneToOne(targetEntity = Member.class, fetch = FetchType.EAGER, cascade = CascadeType.MERGE)
|
||
@JoinColumn(name = "mid")
|
||
Member member;
|
||
}
|
||
```
|
||
|
||
同样的,在应用中还将存在一个用于操作`TokenStore`实体的`TokenStoreRepository`接口。因为对用户令牌的鉴权是由另一个`Realm`类完成的,所以携带用户令牌的`AuthenticationToken`类就需要与承载用户名和密码的`UsernamePasswordToken`类区分开,这里直接采用Shiro内置的`BearerToken`类来承载跟随HTTP请求发来的用户令牌。
|
||
|
||
```java
|
||
@Component
|
||
@RequiredArgsConstructor
|
||
public class TokenRealm extends AuthorizingRealm {
|
||
private final MemberRepository memberRepository;
|
||
private final TokenStoreRepository storeRepository;
|
||
|
||
@PostConstruct
|
||
private void initRealm() {
|
||
super.setCredentialsMatcher(new SimpleCredentialsMatcher());
|
||
}
|
||
|
||
@Override
|
||
public boolean supports(AuthorizationToken token) {
|
||
return token instanceof BearerToken;
|
||
}
|
||
|
||
// 关于授权信息的获取,基本上所有的Realm都是一样的。
|
||
@Override
|
||
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
|
||
var username = (String) principalCollection.getPrimaryPrincipal();
|
||
var roles = memberRepository.findMemberByUsername(username)
|
||
.stream()
|
||
.map(Member::getRoles)
|
||
.flatMap(Collection::stream)
|
||
.map(Role::getKeyword)
|
||
.collect(Collectors.toSet());
|
||
return new SimpleAuthorizationInfo(roles);
|
||
}
|
||
|
||
@Override
|
||
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
|
||
var token = (BearerToken) authenticationToken;
|
||
// 在BearerToken中,无论是principal还是crendentials里保存的都是用户令牌信息
|
||
return storeRepository.findTokenById(token.getToken())
|
||
.filter(t -> t.getExpires().isAfter(LocalDateTime.now()))
|
||
.map(t -> new SimpleAuthenticationInfo(t.getMember().getUsername(), t.getToken(), "TokenRealm"))
|
||
.orElseThrow(() -> new AuthenticationException("用户令牌不存在。"));
|
||
}
|
||
}
|
||
```
|
||
|
||
!!! info "对用户密码的验证"
|
||
在以上两个`Realm`类的示例中,都没有显式对用户提供的认证信息进行密码验证,这一步实际上是在`AuthenticatingRealm`的`getAuthenticationInfo()`方法中利用`assertCredentialsMatch()`来完成的。`AuthenticatingRealm`利用`CredentialsMatcher`来完成用户提供的认证信息的验证。所以在每个自定义的`Realm`中,还需要指定对于用户认证信息的加密或者散列方法。例如对于用于用户密码进行验证的`UsernameRealm`,可以采用SHA-512等散列方法来处理用户密码,而对于`BearerRealm`,则可以直接采用透明比对的方式。
|
||
|
||
与对用户名和密码的验证不同,对于用户令牌的验证并不是在特定的Spring MVC Controller中完成的,而是需要在Shiro的Filter中完成,所以还需要构建一个用于从HTTP请求中提取用户令牌并形成`BearerToken`实例的过滤器。因为这里选择使用了标准的Bearer Token认证方式,所以可以直接使用Shiro内置的`BearerHttpAuthenticationFilter`。
|
||
|
||
#### 完成SecurityManager的构建
|
||
|
||
上面这些类都已经具备以后,现在就需要对之前定义的`WebShiroConfig`类进行一次整理,其中最主要的事情就是把所需要的Filter指派给相应的路径。整理之后的`WebShiroConfig`是下面这个样子。
|
||
|
||
```java
|
||
@Configuration
|
||
@Import({
|
||
ShiroBeanConfiguration.class,
|
||
ShiroAnnotationProcessorConfiguration.class,
|
||
ShiroWebConfiguration.class,
|
||
ShiroWebFilterConfiguration.class,
|
||
ShiroRequestMappingConfig.class
|
||
})
|
||
public class WebShiroConfig {
|
||
private final SecurityManager securityManager;
|
||
|
||
public WebShiroConfig(SecurityManager securityManager) {
|
||
this.securityManager = securityManager;
|
||
SecurityUtils.setSecurityManager(this.securityManager);
|
||
}
|
||
|
||
@Bean(name = "shiroFilterFactoryBean")
|
||
@Autowired
|
||
public ShiroFilterFactoryBean shiroFilterFactory(
|
||
ShiroFilterChainDefainition filterChainDefinition
|
||
) {
|
||
var filterFactory = new ShiroFilterFactoryBean();
|
||
filterFactory.setSecurityManager(this.securityManager);
|
||
|
||
filterFactory.setLoginUrl("/login");
|
||
filterFactory.setFilterChainDefinitionMap(filterChainDefinition.getFilterChainMap());
|
||
return filterFactory;
|
||
}
|
||
|
||
@Bean
|
||
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
|
||
var chainDefinition = new DefaultShiroFilterChainDefinition();
|
||
|
||
// 这里所有路径的声明顺序是有重要意义的,应该遵循先特殊Filter,再匿名Filter,最后通用Filter的顺序,保证路径的匹配顺序
|
||
chainDefinition.addPathDefinition("/logout", "logout");
|
||
chainDefinition.addPathDefinition("/login", "anon");
|
||
// 要求所有需要携带用户令牌的请求都必须经过BearerHttpAuthenticationFilter
|
||
chainDefinition.addPathDefinition("/**", "authcBearer");
|
||
|
||
return chainDefinition;
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 集成Shiro后常见错误的处理
|
||
|
||
##### 所有路径都报404错误
|
||
|
||
出现这种情况的原因是`shiro-spring-boot-starter`在进行自动配置的时候,没有正确的配置`DefaultAdvisorAutoProxyCreator`的Bean,所以只需要在Shiro的配置类中加入这个Bean即可,代码可以仿照以下Bean实例化代码添加。
|
||
|
||
```java
|
||
@Bean
|
||
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
|
||
var creator = new DefaultAdvisorAutoProxyCreator();
|
||
creator.setUsePrefix(true);
|
||
|
||
return creator;
|
||
}
|
||
```
|
||
|
||
### 处理用户登录
|
||
|
||
用户登录主要是一个收集用户提供的认证信息并进行校验的过程,在Shiro提供的Realm加持下,处理用户登录的过程被简化成了只需要组装`AuthenticationToken`,不必再次实现认证信息的校验的过程。以下是一个登录Controller的示例。
|
||
|
||
```java
|
||
@PostMapping("/login")
|
||
@Transactional
|
||
public LoginResponse doLogin(@RequestBody VLoginForm form) {
|
||
var subject = SecurityUtils.getSubject();
|
||
var token = new UsernamePasswordToken(form.getUsername(), form.getPassword());
|
||
try {
|
||
// 这一句就可以用来执行登录过程,只要这个方法不抛出异常,那么登录就是成功的
|
||
subject.login(token);
|
||
Token userToken = tokenService.generate(token);
|
||
storeRepository.save(userToken);
|
||
return LoginResponse.builder().code(200).message("登录成功。").token(userToken.getToken()).build();
|
||
} catch (UnknownAccountException e) {
|
||
return LoginResponse.builder().code(403).message("用户不存在。").build();
|
||
} catch (DisabledAccountException e) {
|
||
return LoginResponse.builder().code(403).message("用户被禁止登录").build();
|
||
} catch (IncorrectCredentialsException e) {
|
||
return LoginResponse.builder().code(403).message("用户名或者密码不正确。").build();
|
||
} catch (Throwable e) {
|
||
e.printStackTrace();
|
||
return LoginResponse.builder().code(500).message("服务器错误").build();
|
||
}
|
||
}
|
||
```
|
||
|
||
### 处理用户登出
|
||
|
||
在Shiro中,用户登出的逻辑已经是比较完善了,但是如果需要在用户登录的时候额外完成其他的操作,那么就需要自行编写一个Controller来进行处理了。比如在下面的示例中,登出操作将删除已经存储的用户令牌。
|
||
|
||
```java
|
||
@PostMapping("/logout")
|
||
@Transactional
|
||
public BaseResponse doLogout() {
|
||
var subject = SecurityUtils.getSubject();
|
||
var token = storeRepository.findTokenByMember((Member) subject.getPrincipal());
|
||
storeRepository.delete(token);
|
||
return BaseResponse.builder().code(200).build();
|
||
}
|
||
```
|
||
|
||
### 控制用户访问
|
||
|
||
在某一个路径上对用户进行控制,可以通过两种方法完成:
|
||
|
||
1. 在`ShiroFilterChainDefinition`实例中定义。
|
||
1. 使用Shiro提供的注解定义。
|
||
|
||
#### 在FilterChainDefinition中定义
|
||
|
||
这种定义方式主要是通过`.addPathDefinition()`方法来完成,这个方法的第二个参数不仅可以定义使用哪个Filter来进行处理,还可以利用逗号分隔的描述声明这条路径所需要的角色或者权限限制。角色或者权限的描述可以使用以下表达式来书写。
|
||
|
||
1. `roles[role_name,role_name]`,定义访问路径所需要具备的角色名称。
|
||
1. `perms[perm_name,perm_name]`,定义访问路径所需要具备的权限名称。
|
||
|
||
!!! info "关于多级权限描述"
|
||
Shiro的权限名称是支持多级描述的,每一级描述之间使用`:`隔开,例如`document:read`和`document:write`。多级权限描述可以使用通配符进行广泛的匹配,例如`document:*`可以匹配`document:read`和`document:write`。
|
||
|
||
例如可以这样来定义一个路径所需的权限。
|
||
|
||
```java
|
||
filterChainDefinition.addPathDefinition("/document/:id", "authc,perms[document:read]")
|
||
```
|
||
|
||
#### 使用注解定义
|
||
|
||
注解一般都是使用在Controller上的,这一点跟Spring Security是一样的。注解中所使用的角色和权限的描述表达式,跟在FilterChainDefinition中是一样的,只是将`[]`中的内容转移到了注解中。Shiro提供了以下两个注解来定义访问某一个Controller所需要的权限。
|
||
|
||
1. `@RequiresRoles()`,定义Controller所需要的角色名称。
|
||
1. `@requiredPermissions()`,定义Controller所需要的权限名称。
|
||
|
||
例如在Controller里需要`document:read`权限的Controller方法就可以这样编写。
|
||
|
||
```java
|
||
@RequiresPermissions("document:read")
|
||
@GetMapping("/document/:id")
|
||
public DocuemtResponse readDocument(@PathVariable String docId) {
|
||
// ...
|
||
}
|
||
```
|