SpringBoot安全访问-JWT
大约 17 分钟
JWT 简介
互联网认证管理
- OAuth2 协议可以有效的解决第三方系统的安全认证处理问题,在超大型的互联网项目之中可以很好的实现登录认证处理,同时也可以方便的实现所有接入客户端的管理

内部项目认证管理
- 在一些小型的项目应用环境之中,使用 OAuth2 来实现统一认证管理就会非常的繁琐而且也会影响到项目的性能。

JWT
- 为了简化 SSO 的实现难度以及第三方客户端整合的接入难度,可以直接利用一个 Token 数据实现用户认证信息的存储,这样在每次进行应用资源访问时只需要传递并验证此 Token 数据项,即可实现分布式的认证管理,这样的操作机制不仅简单,且整合难度较低。最重要的是每一组 Token 数据量较小,这样可以得到更快的网络传输速度

JWT 结构分析
JWT 数据组成结构
- 在实际的项目开发中,JWT 主要是为了实现用户认证数据的处理,所以第三方应用客户端要想进行用户统一登录的操作,只需要传入用户认证所需要的数据信息,即可成功的获取到 Token 令牌,考虑到令牌的安全性以及实用性,在每一个 T 数据中会包含有三类信息项:Header 头部信息、Payload 负载信息、Signature 数字签名

JWT 获取与处理
- 使用 JWT 的结构特点,可以有效的实现用户数据信息的携带,每次进行服务调用时都需要传递此 JWT 数据,目标微服务依靠此数据实现用户登录状态检测,同时也可以根据其保存的用户角色数据,来进行当前操作执行的合法性校验

自定义 JWT 配置
- 在实际项目开发中,不同的项目会存在有不同的数字签名、发布者等数据信息,考虑到可以直接通过 application.yml 配置 JWT 的相关属性内容,随后将 JWT 使用的便捷性,这些属性注入到指定的配置类中

1、
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.12.5'
// https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api
implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1'
2、
ext.versions = [ // 定义所有要使用的版本号
jjwt : '0.9.1', // JWT依赖库
jaxb : '2.3.1', // JAXB依赖库
]
ext.libraries = [ // 定义所有的依赖库
// 以下的配置为JWT所需要的依赖库
'jjwt': "io.jsonwebtoken:jjwt:${versions.jjwt}",
'jaxb-api': "javax.xml.bind:jaxb-api:${versions.jaxb}"
]
3、
project('microboot-jwt') { // 子模块
dependencies { // 配置子模块依赖
compile('org.springframework.boot:spring-boot-starter-web')
compile(libraries.'fastjson')
compile(libraries.'jjwt')
compile(libraries.'jaxb-api')
}
}
4、
spring:
application:
name: microboot-jwt # 应用名称
muyan: # 自定义配置项
config: # 配置项定义
jwt: # 配置JWT相关属性
sign: muyan # JWT证书签名
issuer: MuyanYootk # 证书签发人
secret: www.yootk.com # 加密密钥
expire: 10 # 有效时间(单位:秒)
5、
package com.yootk.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "muyan.config.jwt")
public class JWTConfigProperties { // 保存JWT配置项
private String sign;
private String issuer;
private String secret;
private long expire;
}
6、
package com.yootk;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StartJWTApplication {
public static void main(String[] args) { // 沐言科技:www.yootk.com
SpringApplication.run(StartJWTApplication.class, args); // 程序启动
}
}
7、
package com.yootk.test;
import com.yootk.StartJWTApplication;
import com.yootk.config.JWTConfigProperties;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
@ExtendWith(SpringExtension.class) // Junit5测试工具
@WebAppConfiguration // 表示需要启动Web配置才可以进行测试
@SpringBootTest(classes = StartJWTApplication.class) // 定义要测试的启动类
public class TestJWTConfigProperties {
@Autowired
private JWTConfigProperties configProperties;
@Test
public void testConfig() {
System.out.println(configProperties);
}
}
JWT 数据服务
JWT 数据操作业务
- 在 JWT 数据操作过程中,可以根据图所示的类结构,创建一个专属的 ITokenService 业务接口利用该业务接口提供的方法实现 JWT 数据的创建,由于在微服务访问之前需要进行 JWT 数据的检测,所以在该业务接口中还应该提供有 JWT 数据的校验、解析、刷新(延缓数据有效期)等功能,下面通过具体的实例为读者讲解“jwt”依赖库所提供的操作类和接口的使用。

1、
package com.yootk.service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import javax.crypto.SecretKey;
import java.util.Map;
public interface ITokenService { // 实现JWT的相关操作接口
public SecretKey generalKey(); // 获取当前JWT数据加密KEY
/**
* 生成一个合法的Token数据
* @param id 这个Token的唯一ID(随意存储,本次可以考虑存储用户ID)
* @param subject 所有附加的信息内容,本次直接接收了一个Map,但是最终存储的时候存放JSON
* @return 返回一个有效的Token数据字符串
*/
public String createToken(String id, Map<String, Object> subject);
/**
* 是根据Token的字符串内容解析出其组成的信息(头信息与附加信息)
* @param token 要解析的Token完整数据
* @return Jws接口实例
* @throws JwtException 如果Token失效或者结构错误
*/
public Jws<Claims> parseToken(String token) throws JwtException;
/**
* 校验当前传递的Token数据是否正确
* @param token 要检查的Token数据
* @return true表示合法、false表示无效
*/
public boolean verifyToken(String token);
/**
* Token存在有效时间的定义,所以一定要提供有Token刷新机制
* @param token 原始的Token数据
* @return 新的Token数据
*/
public String refreshToken(String token);
}
2、
package com.yootk.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.yootk.config.JWTConfigProperties;
import com.yootk.service.ITokenService;
import io.jsonwebtoken.*;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Service
public class TokenServiceImpl implements ITokenService {
@Autowired
private JWTConfigProperties jwtConfigProperties; // JWT的相关配置属性
@Value("${application.application.name?:muyan-yootk-token}") // Groovy表达式
private String applicationName;
private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 签名算法
@Override
public SecretKey generalKey() { // 获取加密KEY
byte[] encodedKey = Base64.decodeBase64(Base64.encodeBase64(this.jwtConfigProperties.getSecret().getBytes()));
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
@Override
public String createToken(String id, Map<String, Object> subject) {
Date nowDate = new Date(); // 获取当前的日期时间
// 当前的时间 + 失效时间配置的秒数 = 最终失效的日期时间
Date expireDate = new Date(nowDate.getTime() + this.jwtConfigProperties.getExpire() * 10000);
Map<String, Object> claims = new HashMap<>(); // 附加的Claims信息
claims.put("site", "www.yootk.com"); // 添加信息内容
claims.put("book", "SpringBoot就业编程实战"); // 添加信息内容
claims.put("company", "沐言科技"); // 添加信息内容
Map<String, Object> headers = new HashMap<>(); // 保存的头信息
headers.put("author", "爆可爱的小李老师");
headers.put("module", this.applicationName); // 保存应用的名称
headers.put("desc", "我是一个很普通的老师,喜欢教学,认真搞真正的教育。");
JwtBuilder builder = Jwts.builder().setClaims(claims) // 保存Claims信息
.setHeader(headers) // 保存Headedr信息
.setId(id) // 保存ID内容
.setIssuedAt(nowDate) // 证书签发日期时间
.setIssuer(this.jwtConfigProperties.getIssuer()) // 证书签发者
.setSubject(JSONObject.toJSONString(subject)) // 附加信息
.signWith(this.signatureAlgorithm, this.generalKey()) // 签名算法
.setExpiration(expireDate); // Token失效时间
return builder.compact(); // 生成Token
}
@Override
public Jws<Claims> parseToken(String token) throws JwtException {
if (this.verifyToken(token)) { // 检查当前的Token是否正确
Jws<Claims> claims = Jwts.parser().setSigningKey(this.generalKey()).parseClaimsJws(token);
return claims;
}
return null;
}
@Override
public boolean verifyToken(String token) {
try {
Jwts.parser().setSigningKey(this.generalKey()).parseClaimsJws(token).getBody();
return true;// 没有异常,解析成功
} catch (JwtException exception) {
return false;
}
}
@Override
public String refreshToken(String token) {
if (this.verifyToken(token)) { // 正确的Token是可以进行刷新的
Jws<Claims> claimsJws = this.parseToken(token); // 解析数据
return this.createToken(claimsJws.getBody().getId(), JSONObject.parseObject(claimsJws.getBody().getSubject(), Map.class));
}
return null;
}
}
3、
package com.yootk.test;
import com.alibaba.fastjson.JSONObject;
import com.yootk.StartJWTApplication;
import com.yootk.config.JWTConfigProperties;
import com.yootk.service.ITokenService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@ExtendWith(SpringExtension.class) // Junit5测试工具
@WebAppConfiguration // 表示需要启动Web配置才可以进行测试
@SpringBootTest(classes = StartJWTApplication.class) // 定义要测试的启动类
public class TestTokenService {
@Autowired
private ITokenService tokenService;
private String token = "eyJhdXRob3IiOiLniIblj6_niLHnmoTlsI_mnY7ogIHluIgiLCJtb2R1bGUiOiJtdXlhbi15b290ay10b2tlbiIsImFsZyI6IkhTMjU2IiwiZGVzYyI6IuaIkeaYr-S4gOS4quW-iOaZrumAmueahOiAgeW4iO-8jOWWnOasouaVmeWtpu-8jOiupOecn-aQnuecn-ato-eahOaVmeiCsuOAgiJ9.eyJzdWIiOiJ7XCJyaWRzXCI6XCJVU0VSO0FETUlOO0RFUFQ7RU1QO1JPTEVcIixcIm5hbWVcIjpcIuaykOiogOenkeaKgC3mnY7lhbTljY5cIixcIm1pZFwiOlwibXV5YW5cIn0iLCJzaXRlIjoid3d3Lnlvb3RrLmNvbSIsImJvb2siOiJTcHJpbmdCb2905bCx5Lia57yW56iL5a6e5oiYIiwiaXNzIjoiTXV5YW5Zb290ayIsImNvbXBhbnkiOiLmspDoqIDnp5HmioAiLCJleHAiOjE2MjQ3Njk5NzgsImlhdCI6MTYyNDc1OTk3OCwianRpIjoieW9vdGstNGQ2YzdkMzItZmE5Mi00ZTc4LWJkN2YtNzE1MGMxMDA3MDRlIn0.B7f11ckb4etMTcxzdzTh_1VubQSHnifl43t2-3atrD4";
@Test
public void testCreate() { // 创建Token数据
Map<String, Object> map = new HashMap<>(); // 保存subject数据信息
map.put("mid", "muyan");
map.put("name", "沐言科技-李兴华");
map.put("rids", "USER;ADMIN;DEPT;EMP;ROLE"); // 保存角色数据
String id = "yootk-" + UUID.randomUUID(); // 随机生成ID
System.out.println(this.tokenService.createToken(id, map));
}
@Test
public void testParse() {
Jws<Claims> claims = this.tokenService.parseToken(token); // 解析得到的Token数据
claims.getHeader().forEach((name, value) -> {
System.out.println("【JWT头信息】name = " + name + "、value = " + value);
});
System.err.println("------------------------------------------------------------------");
claims.getBody().forEach((name, value) -> {
System.out.println("【JWT主题信息】name = " + name + "、value = " + value);
});
System.err.println("------------------------------------------------------------------");
Map<String, Object> map = JSONObject.parseObject(claims.getBody().get("sub").toString(), Map.class); // 用户配置的信息
map.entrySet().forEach(entry -> {
System.out.println("【用户数据】key = " + entry.getKey() + "、value = " + entry.getValue());
});
}
@Test
public void testVerifyJWT() {
System.out.println(this.tokenService.verifyToken(token));
}
@Test
public void testRefreshToken() {
System.out.println(this.tokenService.refreshToken(token));
}
}
Token 拦截
Token 验证拦截
- 在 JWT 的操作机制中如果要想安全的实现微服务的访问,则需要在每次请求处理前进如果用户传递的 Token 数据有效,则允许用户访问目标资源。反行 Token 数据的校验,女之,如果 Token 数据无效则应该进行错误信息的显示,而这一操作可以直接基于拦截器的方式实现

1、
package com.yootk.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD}) // 该注解主要用于方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
public @interface JWTCheckToken { // JWT的检查注解
public boolean required() default true; // 是否要启用Token检查
}
2、
package com.yootk.action;
import com.yootk.annotation.JWTCheckToken;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/message/*") // 父路径
public class MessageAction {
@RequestMapping("echo")
@JWTCheckToken // 这个资源需要被检查
public Object echo(String msg) {
return "【ECHO】" + msg;
}
}
3、
package com.yootk.interceptor;
import com.yootk.annotation.JWTCheckToken;
import com.yootk.service.ITokenService;
import jdk.jfr.Frequency;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
public class JWTAuthenticationInterceptor implements HandlerInterceptor { // 认证拦截器
// Token可以通过参数传递也可以通过头信息传递
private static final String TOKEN_NAME = "yootkToken"; // Token参数名称
@Autowired
private ITokenService tokenService; // Token业务接口
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) { // 不处理拦截操作
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler; // 类型转换
Method method = handlerMethod.getMethod(); // 获取当前要执行的Action方法反射对象
if (method.isAnnotationPresent(JWTCheckToken.class)) { // 判断该方法上是否提供有指定的注解
JWTCheckToken checkToken = method.getAnnotation(JWTCheckToken.class); // 获取指定注解
if (checkToken.required()) { // true表示要进行Token检查
String token = this.getToken(request); // 获取Token数据
if (!this.tokenService.verifyToken(token)) { // 验证失败
throw new RuntimeException("Token数据无效,无法访问。");
}
}
}
return true;
}
public String getToken(HttpServletRequest request) {
String token = request.getParameter(TOKEN_NAME); // 通过参数获取头信息
if (token == null || "".equals(token)) { // 没有接收到Token
token = request.getHeader(TOKEN_NAME); // 通过头信息获取
}
return token;
}
}
4、
package com.yootk.config;
import com.yootk.interceptor.JWTAuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer { // 拦截配置类
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(this.getJWTAuthenticationInterceptor()).addPathPatterns("/**");
}
@Bean
public HandlerInterceptor getJWTAuthenticationInterceptor() {
return new JWTAuthenticationInterceptor();
}
}
5、
localhost:8080/message/echo?msg=www.yootk.com&yootkToken=eyJhdXRob3IiOiLniIblj6_niLHnmoTlsI_mnY7ogIHluIgiLCJtb2R1bGUiOiJtdXlhbi15b290ay10b2tlbiIsImFsZyI6IkhTMjU2IiwiZGVzYyI6IuaIkeaYr-S4gOS4quW-iOaZrumAmueahOiAgeW4iO-8jOWWnOasouaVmeWtpu-8jOiupOecn-aQnuecn-ato-eahOaVmeiCsuOAgiJ9.eyJzdWIiOiJ7XCJyaWRzXCI6XCJVU0VSO0FETUlOO0RFUFQ7RU1QO1JPTEVcIixcIm5hbWVcIjpcIuaykOiogOenkeaKgC3mnY7lhbTljY5cIixcIm1pZFwiOlwibXV5YW5cIn0iLCJzaXRlIjoid3d3Lnlvb3RrLmNvbSIsImJvb2siOiJTcHJpbmdCb2905bCx5Lia57yW56iL5a6e5oiYIiwiaXNzIjoiTXV5YW5Zb290ayIsImNvbXBhbnkiOiLmspDoqIDnp5HmioAiLCJleHAiOjE3MTA2NjkzMTcsImlhdCI6MTcxMDY2OTIxNywianRpIjoieW9vdGstMjhlOTZlMTQtMDZmZS00NTlkLWIwODktNGRiNDhmNTQ1NjRiIn0.yHq9v7k3RIWNG6fin4s0SsxtsNlPNTZZrDS5i0EDZd8
Shiro 整合简介
Shiro
- Shiro 是一款被广泛使用的认证与授权安全框架,是由 Apache 推出并维护的,与 SpringSecurity 不同的是,Shiro 的实现机制更加的简单,可以帮助开发者轻松的实现:授权管理、数据加密、会话管理、数据缓存等功能,而实现这些功能主要依认证管理、:靠以下三个核心组件:
- Subject:当前操作的主体,在 Shiro 中主体是一个抽象的概念,可能是用户,也可能是一个机器人;Realm:Shiro 通过 Realm 实现用户认证与授权数据信息的获取;
- SecurityManager:Shiro 安全管理器,所有与安全有关的操作都与 SecurityManager 交互,可以实现整个项目中的 Realm、缓存、Cookie、Session 等核心组件的管理;
SpringMVC 与 Shiro 整合
- Shiro 是基于 Filter 过滤实现的安全访问控制框架在与 SpringMVC 框架整合时,除了可以依据过滤策略实现认证与授权信息检查之外,也可以通过 ProxyProcessorSupport 切面控制结构,基于注解的方式实现控制层与业务层的安全访问控制
Shiro 用户认证
Shiro 实现结构
- Shiro 的处理机制主要是依赖于 Realm 实现用户认证以及授权数据的加载处理,同时基于过滤器的检查策略实现资源的安全保护,为便于理解下面将采用一种固定认证信息的模式(用户名任密码以及授权信息相同)来实现基于前后端分离设计的 Shiro 认证管理意、

1、
// https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-web-starter
implementation group: 'org.apache.shiro', name: 'shiro-spring-boot-web-starter', version: '1.7.1'
2、
ext.versions = [ // 定义所有要使用的版本号
springboot : '2.4.3', // SpringBoot版本号
junit : '5.7.1', // 配置JUnit测试工具的版本编号
junitPlatformLauncher : '1.7.1', // JUnit测试工具运行平台版本编号
lombok : '1.18.18', // Lombok插件对应的版本号
fastjson : '1.2.75', // FastJSON组件对应的版本号
jackson : '2.12.2', // 配置Jackson相关依赖库
itextpdf : '5.5.13.2', // PDF文件生成的依赖库
easypoi : '4.3.0', // 生成Excel处理的依赖库
hibernateValidator : '6.2.0.Final', // JSR303验证库
prometheus : '1.6.5', // Prometheus监控数据版本
shedlock : '4.23.0', // ShedLock组件
springDataRedis : '2.4.5', // SpringDataRedis版本
commonsPool2 : '2.9.0', // 连接池版本
jaxwsRi : '2.3.3', // JDK-WS依赖
cxf : '3.4.3', // WEBService开发框架版本
mysql : '8.0.25', // MySQL驱动的版本
druid : '1.2.6', // Druid版本
springJdbc : '5.3.7', // SpringJDBC版本
mybatis : '3.5.7', // MyBatis的开发版本
mybatisSpringBoot : '2.2.0', // Mybatis-SpringBoot整合依赖
mybatisPlus : '3.4.3', // MyBatisPlus依赖版本
springSecurityOAuth2 : '2.4.3', // OAuth2版本
jjwt : '0.9.1', // JWT依赖库
jaxb : '2.3.1', // JAXB依赖库
shiro : '1.7.1', // Shiro版本编号
]
ext.libraries = [ // 定义所有的依赖库
// 以下的配置为SpringBoot项目所需要的核心依赖
'spring-boot-gradle-plugin': "org.springframework.boot:spring-boot-gradle-plugin:${versions.springboot}",
// 以下的配置为与项目用例测试有关的依赖
'junit-jupiter-api': "org.junit.jupiter:junit-jupiter-api:${versions.junit}",
'junit-vintage-engine': "org.junit.vintage:junit-vintage-engine:${versions.junit}",
'junit-jupiter-engine': "org.junit.jupiter:junit-jupiter-engine:${versions.junit}",
'junit-platform-launcher': "org.junit.platform:junit-platform-launcher:${versions.junitPlatformLauncher}",
'junit-bom': "org.junit:junit-bom:${versions.junit}",
// 以下的配置为Lombok组件有关的依赖
'lombok': "org.projectlombok:lombok:${versions.lombok}",
// 以下的配置为FastJSON组件有关的依赖
'fastjson': "com.alibaba:fastjson:${versions.fastjson}",
// 以下的配置为Jackson将输出转换为XML有关的依赖
'jackson-dataformat-xml': "com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${versions.jackson}",
'jackson-databind': "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}",
'jackson-annotations': "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}",
// 以下的配置为ITextPDF输出的有关依赖配置
'itextpdf': "com.itextpdf:itextpdf:${versions.itextpdf}",
// 以下的配置为生成Excel文件有关的依赖配置
'easypoi-spring-boot-starter': "cn.afterturn:easypoi-spring-boot-starter:${versions.easypoi}",
// 以下的配置为HibernateValidator实现的JSR303验证标准依赖
'hibernate-validator': "org.hibernate.validator:hibernate-validator:${versions.hibernateValidator}",
// 以下的配置为Prometheus监控数据操作
'micrometer-registry-prometheus': "io.micrometer:micrometer-registry-prometheus:${versions.prometheus}",
// 以下的配置为ShedLock分布式任务调度组件
'shedlock-spring': "net.javacrumbs.shedlock:shedlock-spring:${versions.shedlock}",
'shedlock-provider-redis-spring': "net.javacrumbs.shedlock:shedlock-provider-redis-spring:${versions.shedlock}",
// 以下的配置为Redis缓存组件
'spring-boot-starter-data-redis': "org.springframework.boot:spring-boot-starter-data-redis:${versions.springDataRedis}",
'commons-pool2': "org.apache.commons:commons-pool2:${versions.commonsPool2}",
// 以下的配置为WebService开发所需要的依赖:
'jaxws-ri': "com.sun.xml.ws:jaxws-ri:${versions.jaxwsRi}",
'cxf-spring-boot-starter-jaxws': "org.apache.cxf:cxf-spring-boot-starter-jaxws:${versions.cxf}",
'cxf-rt-transports-http': "org.apache.cxf:cxf-rt-transports-http:${versions.cxf}",
// 以下的配置为数据库开发所需要的依赖:
'mysql-connector-java': "mysql:mysql-connector-java:${versions.mysql}",
'druid-spring-boot-starter': "com.alibaba:druid-spring-boot-starter:${versions.druid}",
'spring-jdbc': "org.springframework:spring-jdbc:${versions.springJdbc}",
'druid': "com.alibaba:druid:${versions.druid}",
// 以下的配置为MyBatis开发框架所需要的依赖:
'mybatis': "org.mybatis:mybatis:${versions.mybatis}",
'mybatis-spring-boot-starter': "org.mybatis.spring.boot:mybatis-spring-boot-starter:${versions.mybatisSpringBoot}",
// 以下的配置为MybatisPlus开发框架所需要的依赖:
'mybatis-plus': "com.baomidou:mybatis-plus:${versions.mybatisPlus}",
'mybatis-plus-boot-starter': "com.baomidou:mybatis-plus-boot-starter:${versions.mybatisPlus}",
'spring-security-oauth2-autoconfigure' : "org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:${versions.springSecurityOAuth2}",
// 以下的配置为JWT所需要的依赖库
'jjwt': "io.jsonwebtoken:jjwt:${versions.jjwt}",
'jaxb-api': "javax.xml.bind:jaxb-api:${versions.jaxb}",
// 以下的配置为Shiro所需要的依赖库
'shiro-spring-boot-web-starter': "org.apache.shiro:shiro-spring-boot-web-starter:${versions.shiro}"
]
3、
project('microboot-shiro') { // 子模块
dependencies { // 配置子模块依赖
compile('org.springframework.boot:spring-boot-starter-web')// 引入SpringBoot依赖
compile(libraries.'shiro-spring-boot-web-starter')// 引入Shiro依赖
}
}
4、
package com.yootk.realm.matcher;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
public class DefaultCredentialsMatcher extends SimpleCredentialsMatcher { // 密码匹配器
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String defaultPassword = super.toString(token.getCredentials()); // 获取原始的输入密码
return "yootk".equals(defaultPassword); // 实现密码匹配
}
}
5、
package com.yootk.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import java.util.Set;
public class MemberRealm extends AuthorizingRealm { // 定义Real处理
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 登录认证处理
// 此时匹配的用户密码为“yootk”,这个密码直接采用明文的方式传递到后面的密码匹配器之中
return new SimpleAuthenticationInfo(token.getPrincipal(), "yootk", "memberRealm");
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // 授权处理
// 这个操作是需要通过数据库进行加载的,同时考虑到性能问题又需要通过缓存进行数据存储
Set<String> roles = Set.of("message", "member");
Set<String> actions = Set.of("message:echo", "message:list", "member:add", "member:list", "member:delete", "member:edit");
SimpleAuthorizationInfo authz = new SimpleAuthorizationInfo();
authz.setRoles(roles); // 保存角色
authz.setStringPermissions(actions); // 保存权限
return authz;
}
}
6、
package com.yootk.action;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@RestController
public class MemberAction { // 创建控制器类
@RequestMapping("/login_handle") // 定义访问路径
public Object loginHandler(UsernamePasswordToken token, HttpServletRequest request) { // 所有的信息通过Token类接收
Map<String, Object> result = new HashMap<>(); // 保存最终的响应结果
try {
SecurityUtils.getSubject().login(token); // 登录处理
result.put("token", "yootk-jwt-token"); // 返回Token的数据项
result.put("session-id", request.getSession().getId()); // 返回当前操作的SessionID
} catch (Exception e) {
result.put("error", e.getMessage()); // 返回错误信息
}
return result;
}
}
7、
package com.yootk.config;
import com.yootk.realm.MemberRealm;
import com.yootk.realm.matcher.DefaultCredentialsMatcher;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig { // 定义Shiro的配置类
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilter(
org.apache.shiro.mgt.SecurityManager securityManager) { // 定义Shiro过滤器
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);// 配置
Map<String, String> filterChain = new LinkedHashMap<>(); // 过滤器是有顺序的
filterChain.put("/admin/**", "authc"); // 进行过滤路径配置
factoryBean.setFilterChainDefinitionMap(filterChain); // 配置访问路径
return factoryBean;
}
@Bean(name = "authorizer")
public MemberRealm getMemberRealm() {
MemberRealm realm = new MemberRealm();
realm.setCredentialsMatcher(new DefaultCredentialsMatcher()); // 密码匹配器
return realm;
}
}
8、
server:
port: 80
9、
package com.yootk;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StartShiroApplication {
public static void main(String[] args) {
SpringApplication.run(StartShiroApplication.class, args); // 运行SpringBoot程序
}
}
10、
curl -X POST -d "username=muyan&password=yootk" "http://localhost/login_handle"
Shiro 访问拦截
Shiro 认证与授权
- 最重要的一项就是要实现认证检测以及授权管理,这样使用 Shiro 除了登录认证之外才可以保证应用资源的安全性,由于本次是基于前后端分离的结构设计,所以开发者就需要根据登录时所获取到的 sessionld 来实现处理

1、
package com.yootk.action;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/admin/message/*") // 定义父路径
public class MessageAction {
@RequestMapping("echo")
@RequiresPermissions("message:echo") // 授权判断
public Object echo(String msg) {
return "【ECHO】" + msg;
}
@RequiresPermissions("message:list") // 授权判断
public Object list(String msg) {
List<String> list = new ArrayList<>();
for (int x = 0; x < 10; x ++) {
list.add("【LIST】" + msg);
}
return list;
}
}
2、
package com.yootk.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.session.mgt.WebSessionKey;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
public class ShiroAuthFilter extends FormAuthenticationFilter { // 认证过滤器
private static final String COOKIE_SESSION_ID = "session-id"; // 要获取保存在客户端的SessionID
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest req = (HttpServletRequest) request; // 强制转型,变为HTTP内置对象
String sessionId = req.getHeader(COOKIE_SESSION_ID); // 通过头信息获取内容
if (sessionId == null || "".equals(sessionId)) { // 头信息没有具体内容
sessionId = req.getParameter(COOKIE_SESSION_ID); // 通过参数再获取一次
}
if (sessionId != null) { // 要进行进一步的处理
SessionKey key = new WebSessionKey(sessionId, request, response); // 将接收到的SessionID进行包装
org.apache.shiro.mgt.SecurityManager securityManager = SecurityUtils.getSecurityManager(); // 获取安全管理器
try {
Subject.Builder builder = new Subject.Builder(securityManager);
builder.sessionId(sessionId); // 绑定SessionID
Subject subject = builder.buildSubject(); // 构建用户内容
ThreadContext.bind(subject); // 绑定在容器之中
Session session = securityManager.getSession(key); // 获取Session内容
return session != null; // true允许访问
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
return false; // 访问拒绝
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 如果以上的过滤访问处理返回的是一个False,则触发此方法,这个方法用于进行错误显示
HttpServletResponse resp = (HttpServletResponse) response;
resp.setContentType("application/json; charset=utf-8");
resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // HTTP响应状态码
PrintWriter out = resp.getWriter(); // 获取响应输出流
Map<String, Object> map = new HashMap<>() ; // 保存响应数据信息
map.put("status", HttpServletResponse.SC_UNAUTHORIZED);
map.put("message", "用户未登录,无法进行资源访问!");
out.write(new ObjectMapper().writeValueAsString(map)); // 使用Jacks工具将对象转为JSON数据
out.close(); // 关闭输出流
return false; // 请求拦截
}
}
3、
package com.yootk.config;
import com.yootk.filter.ShiroAuthFilter;
import com.yootk.realm.MemberRealm;
import com.yootk.realm.matcher.DefaultCredentialsMatcher;
import org.apache.commons.collections.map.LinkedMap;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig { // 定义Shiro的配置类
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilter(
org.apache.shiro.mgt.SecurityManager securityManager) { // 定义Shiro过滤器
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);// 配置
Map<String, String> filterChain = new LinkedHashMap<>(); // 过滤器是有顺序的
filterChain.put("/admin/**", "authc"); // 进行过滤路径配置
// 添加扩展的过滤器配置集合
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("authc", new ShiroAuthFilter()); // 服务的整合
factoryBean.setFilters(filterMap); // 保存过滤器
factoryBean.setFilterChainDefinitionMap(filterChain); // 配置访问路径
return factoryBean;
}
@Bean(name = "authorizer")
public MemberRealm getMemberRealm() {
MemberRealm realm = new MemberRealm();
realm.setCredentialsMatcher(new DefaultCredentialsMatcher()); // 密码匹配器
return realm;
}
}
4、
server:
port: 80
shiro:
userNativeSessionManager: true # 允许采用非HTTP的模式进行访问
5、
curl -X POST -d "msg=www.yootk.com" "http://localhost/admin/message/echo"
6、
curl -X POST -d "username=muyan&password=yootk" "http://localhost/login_handle"
7、
curl -X POST -d "msg=www.yootk.com&session-id=3d4cbe98-3947-455a-a8eb-95efd90e7330" "http://localhost/admin/message/echo"
demo
