跳至主要內容

SpringBoot安全访问-JWT

wangdx大约 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


上次编辑于: