Spring Security的项目中集成JWT Token令牌安全访问后台API

news2024/11/24 6:41:14

Spring Security的项目中集成JWT Token令牌安全访问后台API

  • 引言
  • JWT 简介
    • jwt token 的适用场景
    • jwt 的结构
    • 完整jwt
    • jwt 的使用方式
    • 客户端获取jwt令牌访问受保护资源的具体流程
  • Spring Security 安全框架下使用jwt token
    • 新建一个spring boot项目
    • 加入spring security 和 jwt 相关依赖项
    • 项目配置文件
    • 日志打印配置
    • 启动类
    • JWT相关API
    • 新建Jwt令牌工具类
    • 新建JwtToken认证过滤器
    • Spring Security配置类中配置登录成功后返回jwt 令牌
    • 测试效果
    • 测试生成jwt令牌
    • 测试通过jwt令牌认证与鉴权
  • 参考文章

引言

最近接了一个私活项目,后台使用的是Spring Boot脚手架搭建的,认证和鉴权框架用的Spring Security。同时为了确保客户端安全访问后台服务的API,需要用户登录成功之后返回一个包含登录用户信息的jwt token, 用于调用其他接口时将此jwt token携带在请求头中作为调用者的认证信息。最近一个多月一方面在忙着做这个项目,另一方面恰好遇上了精彩的世界杯,也没怎么发文了。很多时候真的深感写篇原创文章比单纯的敲代码麻烦多了,但是好久不更文还是要检讨一下自己的惰性,客服自身的惰性是每个想要突破自我、不甘平庸的普通人的一辈子都不能松懈的重任。

JWT 简介

首先,让我们来补一下jwt的知识。jwt token 的全称叫JSON Web Token ,主要用于在各方之间以JSON 对象方式安全地传输信息。此信息是数字签名的,可以验证和信任,JWT 可以使用密钥(使用 HMAC 算法)或使用 RSAECDSA 的公钥/私钥对进行签名。

虽然 JWT 可以加密以在各方之间提供保密性,但我们将专注于签名令牌。签名的令牌可以验证其中包含的声明的完整性,而加密的令牌会向其他方隐藏这些声明。当使用公钥/私钥对对令牌进行签名时,只有持有私钥的一方才可以签署。

jwt token 的适用场景

  • 鉴权(Authorization):这是最常见的场景。用户登录后,每个后续请求都将包含 JWT,从而允许用户访问该令牌允许的路由、服务和资源。 单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小并且能够在不同的域中轻松使用。

  • 信息交换(Information Exchange):JWT令牌是在各方之间安全传输信息的好方法。 因为可以对 JWT 进行签名(例如,使用公钥/私钥对),所以可以确定发件人就是他们所说的那个人。此外,由于使用 headerpayload 计算签名,还可以验证内容是否被篡改。

jwt 的结构

JWT 由headerpayloadsignature三部分组成,以 . 分割:

  • header:通常由令牌的类型(即 JWT)以及正在使用的签名算法(例如 HMAC SHA256 或 RSA)两部分组成;

    {
    "alg": "HS256",
    "typ": "JWT"
    }
    

    然后,这个 JSON 被 Base64Url 编码以形成 JWT 的第一部分。

  • payload: 有效负载。其中包含声明,声明是关于实体(通常是用户)和附加数据的陈述。 声明分为三种类型:registered, public, private claims.

    注册(registered)声明:这是一组预定义的声明,不是强制性的,但建议使用。以提供一组有用的、可互操作的声明。比如:issue(发行人)、expired(到期时间)、subject(主题)、aud(受众)等。

    公共(public)声明:这些可以由使用人随意定义。 但是为了避免冲突,应该在jwt token 注册中定义,或者定义为包含抗冲突命名空间的 URI。

    私有(private)声明:这些是为在同意使用它们的各方之间共享信息而创建的自定义声明,既不是注册声明也不是公共声明。

    示例如下:

    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
    }
    

    然后对有效负载进行 Base64Url 编码以形成 JSON Web 令牌的第二部分

    注意,对于已签名的令牌,此信息虽然受到保护以防篡改,但任何人都可以读取。除非已加密,否则请勿将机密信息放入 JWT 的有效负载或标头元素中。

  • Signature: 要创建签名部分,必须获取已编码的标头(header)、编码的有效负载(payload)、密钥、header中指定的算法,并对其进行签名。

​ 例如,如果您想使用 HMAC SHA256 算法,签名将通过以下方式创建:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

签名用于验证信息在传输过程中是否被篡改,并且在使用私钥签名令牌的情况下,它还可以验证 JWT 的发送者是否正确。

完整jwt

由三个 . 分隔的 Base64-URL 字符串,可以在 HTML 和 HTTP 环境中轻松传递,相对于基于 XML 的标准(如 SAML)则更紧凑。

下面显示了一个 JWT,该 JWT 具有前面介绍过的headerpayload编码,并使用密钥签名

jwt_token
我们可以在 jwt.io Debugger 网站来解码、验证和生成 JWT。
gen_jwt_token

jwt 的使用方式

在身份校验中,当用户成功登录,将返回一个 JSON Web Token。由于令牌是凭据,因此必须非常小心以防止出现安全问题。

通常令牌需要设置一个过期时间,超过过期时间则令牌失效,需要置换新的令牌。

由于缺乏安全性,不应该将敏感的会话数据存储在浏览器中。每当用户需要访问受保护的路由或资源时,用户代理应该发送jwt,通常在 Authorization header 中使用 Bearer 模式。 header 的内容应如下所示:

Authorization: Bearer <token>

某些情况下,这可以是一种无状态授权机制。服务器的受保护路由将检查 Authorization header 中是否存在有效的 JWT,如果存在,则允许用户访问受保护的资源。如果 JWT 包含必要的数据,则可能会减少查询数据库以进行某些操作的需要,尽管情况并非总是如此。

如果 tokenAuthorization header,跨域 Cross-Origin Resource Sharing (CORS)不是问题,因为它不使用 cookies

客户端获取jwt令牌访问受保护资源的具体流程

jwt_auth_principle
1) 用户在在客户端使用用户名/密码登录;

2)服务端使用密钥生成一个JWT令牌;

3)服务端将生存的jwt令牌返回给浏览器;

4)用户拿到jwt 令牌放到Authentication参数对应的请求头中访问服务端受保护的资源和API;

5)服务端校验签名,从jwt令牌中解析获取用户信息;

6)服务端校验签名通过并从jwt令牌中解析出用户信息,则返回API的成功响应信息给客户端

Spring Security 安全框架下使用jwt token

在非spring security框架下的spring boot项目中使用jwt令牌鉴权,我们只需要新建一个拦截器或者Servlet过滤器解析jwt token信息就行了,解析成功就放行请求,解析失败则返回403权限不足信息就行了。但是在Spring Security 框架中本身就自动适配了很多个过滤器,并组成了一个过滤器链,因此我们也需要新建一个解析jwt token的过滤器加入过滤器链中才行。

新建一个spring boot项目

使用IDEA新建spring boot项目的同时添加一些必要的依赖jar包,如spring mvc、mysql驱动、druid数据源和fast-json及代码简洁工具lombok等

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.bonus</groupId>
    <artifactId>bonus-backend</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>bonus-backend</name>
    <description>bonus-backend</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
       <!--spring web mvc依赖 -->
       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--修改配置文件自动生效依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <!--druid 数据源依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.8</version>
        </dependency>
        <!--阿里json工具依赖 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.79</version>
        </dependency>
        <!--mysql驱动包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
            <scope>runtime</scope>
        </dependency>
        <!--代码简洁工具lombok依赖-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
     <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
</project>

加入spring security 和 jwt 相关依赖项

在项目的pom.xml文件的dependencies标签中加入

        <!--加解密依赖-->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.11</version>
        </dependency>
        <!--持久层框架mybatis-plus依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.2</version>
        </dependency>
       <!--spring security依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>2.2.7.RELEASE</version>
        </dependency>
        <!--jwt token依赖-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.7.0</version>
        </dependency>

项目配置文件

application-porperties

server.servlet.context-path=/bonus
spring.profiles.active=dev

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

# mybatis-plus config
mybatis-plus.mapper-locations=classpath*:/mapper/**/*.xml
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

# twelve.zodiac
twelve.zodiac.mouse=03,15,27,39
twelve.zodiac.cow=02,14,26,38
twelve.zodiac.tiger=01,13,25,37,49
twelve.zodiac.rabbit=12,24,36,48
twelve.zodiac.dragon=11,23,35,47
twelve.zodiac.snake=10,22,34,46
twelve.zodiac.horse=09,21,33,45
twelve.zodiac.sheep=08,20,32,44
twelve.zodiac.monkey=07,19,31,43
twelve.zodiac.chicken=06,18,30,42
twelve.zodiac.dog=05,17,29,41
twelve.zodiac.pig=04,16,28,40

application-dev.properties

server.address=127.0.0.1
server.port=8090

spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.url=jdbc:mysql://localhost:3306/bonus?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.druid.username=bonus_user
spring.datasource.druid.password=tiger2022@
spring.datasource.druid.validation-query=select 1 from dual
#spring.datasource.druid.connect-properties

# redis config
spring.redis.client-name=redis-client
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0

日志打印配置

log4j.properties

log4j.rootLogger=DEBUG,stdout
log4j.logger.com.baomidou.mybatisplus=DEBUG
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p %d %C: %m%n 

启动类

@SpringBootApplication
@EnableConfigurationProperties
@EnableGlobalMethodSecurity(prePostEnabled=true, securedEnabled=true, jsr250Enabled=true)
public class BonusBackendApplication {

    public static void main(String[] args) {
        SpringApplication.run(BonusBackendApplication.class, args);
    }

}

启动类中除了加上@SpringBootApplication注解之外,还加上了开启配置属性生效的注解@EnableConfigurationProperties以及全局安全访问注解@EnableGlobalMethodSecurity进行动态权限校验

JWT相关API

用于生成jwt token 和从 jwt token中解析出用户信息的相关API都在com.auth0.jwt.JWTcom.auth0.jwt.JWTCreator两个类中。

JWT 类中的API方法

  • public JWT(): JWT类的构造方法;

  • public static Builder create(): 创建jwt token的构建器, 返回对象为JWTCreator类中的静态内部类Builder

  • public DecodedJWT decodeJwt(String token): 解析jwt token方法

  • public static DecodedJWT decode(String token) : 静态解析jwt token方法

  • public static Verification require(Algorithm algorithm): 通过算法构造Verification对象静态方法, Verification类主要用来校验jwt令牌是否有效

JWTCreator类中的API方法

静态内部类Builder主要用于构造headerpayload中 的内容, 该静态类主要提供一些列withXXX方法用于指定相应的键值对内容,主要有一下API方法:

  • public JWTCreator.Builder withHeader(Map<String, Object> headerClaims): 构造header代表的键值对集合;
  • public JWTCreator.Builder withKeyId(String keyId): 指定令牌header中的kid的值;
  • public JWTCreator.Builder withIssuer(String issuer): 指定令牌发行者;
  • public JWTCreator.Builder withSubject(String subject): 指定令牌主题;
  • public JWTCreator.Builder withAudience(String... audience): 指定令牌受众,通过该方法可以将令牌授予有限数量的用户;
  • public JWTCreator.Builder withExpiresAt(Date expiresAt): 指定令牌过期日期;
  • public JWTCreator.Builder withNotBefore(Date notBefore): 指定令牌不能早于某个日期使用;
  • public JWTCreator.Builder withIssuedAt(Date issuedAt): 指定令牌签发日期;
  • public JWTCreator.Builder withJWTId(String jwtId): 指定令牌id;
  • public JWTCreator.Builder withClaim(String name, Boolean value): 指定payload中的键值对,值为布尔类型;
  • public JWTCreator.Builder withClaim(String name, Integer value): 指定payload中的键值对,值为Integer类型;
  • public JWTCreator.Builder withClaim(String name, Long value) : 指定payload中的键值对,值为Long类型;
  • public JWTCreator.Builder withClaim(String name, Double value): 指定payload中的键值对,值为Double类型;
  • public JWTCreator.Builder withClaim(String name, String value): 指定payload中的键值对,值为String类型;
  • public JWTCreator.Builder withClaim(String name, Date value): 指定payload中的键值对,值为Date类型;
  • public JWTCreator.Builder withArrayClaim(String name, String[] items): 指定payload中的键值对,值为String数组类型;
  • public JWTCreator.Builder withArrayClaim(String name, Integer[] items): 指定payload中的键值对,值为Integer数组类型;
  • public JWTCreator.Builder withArrayClaim(String name, Long[] items): 指定payload中的键值对,值为Long数组类型;
  • public String sign(Algorithm algorithm) : 签名方法,通过算法签名,得到完整的jwt token内容方法

algorithm算法对象可通过静态方法Algorithem#HMAC256或者Algorithem#HMAC512方法创建,入参为一个String类型的密钥

JWTDecoder类中的API方法

JWTDecoder类为DecodedJWT类的实现类,主要用来从解析jwt令牌后的对象中获取想要的字段信息

  • public String getAlgorithm(): 获取签名算法名称;

  • public String getType(): 获取jwt令牌的类型,默认为jwt;

  • public String getKeyId(): 获取jwt 令牌header中的kid对应的值;

  • public Claim getHeaderClaim(String name): 获取header中指定名字的Claim, 它可以进一步把value代表的数据转成各种数据类型;

  • public String getIssuer(): 获取jwt令牌的签发人;

  • public String getSubject():获取jwt令牌的主题;

  • public List<String> getAudience(): 获取jwt 令牌的受众;

  • public Date getExpiresAt(): 获取jwt令牌过期时间;

  • public Date getNotBefore(): 获取令牌不能早于使用的时间;

  • public String getId(): 获取令牌id;

  • public Claim getClaim(String name): 获取指定名字Claim

  • public Map<String, Claim> getClaims(): 获取jwt令牌中的Claim键值对集合;

  • public String getHeader(): 获取jwt令牌中的header部分内容;

  • public String getPayload(): 获取jwt令牌中的payload部分内容;

  • public String getSignature(): 获取jwt 令牌中签名部分内容;

  • public String getToken(): 还原jwt令牌内容;

新建Jwt令牌工具类

利用JWT相关API我们新建了一个JwtTokenUtil的工具类用于生成jwt令牌

public class JwtTokenUtil {

    // 密钥
    private static final String SECRET = "bonusBACKEND2022$";

    // 过期时间7天
    private static final int EXPIRE_SECONDS = 7*24*3600;

    private final static Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);

    /**
     * 生成token方法
     * @param memInfoMap
     * @return jwtToken
     */
    public static String genAuthenticatedToken(Map<String, Object> memInfoMap){
        List<GrantedAuthority> authorities = (List<GrantedAuthority>) memInfoMap.get("authorities");
        String authorityStr = null;
        if(authorities!=null && authorities.size()>0){
            StringBuffer buffer = new StringBuffer();
            for(int i=0; i<authorities.size()-1; i++){
                buffer.append(authorities.get(i).getAuthority()).append(",");
            }
            buffer.append(authorities.get(authorities.size()-1).getAuthority());
            authorityStr = buffer.toString();
        }
        String[] authorityArray = authorityStr!=null?authorityStr.split(","):null;
        Calendar nowTime = Calendar.getInstance();
        //过期时间
        nowTime.add(Calendar.SECOND, EXPIRE_SECONDS);
        Date expireDate = nowTime.getTime();
        String jwtToken = JWT.create().withJWTId(UUID.randomUUID().toString().replaceAll("-", ""))
                .withClaim("memId", (Long) memInfoMap.get("memId"))
                .withClaim("memAccount", (String) memInfoMap.get("memAccount"))
                .withClaim("memPwd", (String) memInfoMap.get("memPwd"))
                .withClaim("totalCreditAmount", ((BigDecimal) memInfoMap.get("totalCreditAmount")).doubleValue())
                .withClaim("usedCreditAmount", ((BigDecimal) memInfoMap.get("usedCreditAmount")).doubleValue())
                .withClaim("remainCreditAmount", ((BigDecimal) memInfoMap.get("remainCreditAmount")).doubleValue())
                .withArrayClaim("authorities", authorityArray)
                .withIssuedAt(new Date(System.currentTimeMillis()))
                .withExpiresAt(expireDate)
                .sign(Algorithm.HMAC256(SECRET));
        return jwtToken;
    }
}

UserDetailService#loadUserByUsername

@Service
public class MemInfoServiceImpl extends ServiceImpl<MemInfoMapper, MemInfoDTO> implements MemInfoService {
 private final static Logger logger = LoggerFactory.getLogger(MemInfoServiceImpl.class);
    @Resource
    private MyPasswordEncoder passwordEncoder;
    @Resource
    private RoleInfoService roleInfoService;
    
    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MemInfoDTO memInfoDTO = this.baseMapper.getMemInfoByAccount(username);
        if(memInfoDTO==null){
            throw  new UsernameNotFoundException("Username" + username + "is invalid!");
        }
        // 获取用户角色列表
        List<RoleInfoDTO> roleInfoDTOList = roleInfoService.getRolesByMemId(memInfoDTO.getMemId());
        if(roleInfoDTOList.size()>0){
            for(RoleInfoDTO roleInfoDTO: roleInfoDTOList){
                SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" + roleInfoDTO.getRoleName().toUpperCase());
                memInfoDTO.getAuthorities().add(grantedAuthority);
            }
        }
        return memInfoDTO;
    }

MemInfoDTO类源码如下:

@Data
@TableName("bonus_mem_info")
@ApiModel(value="MemInfoDTO", description = "会员DTO")
@Validated
public class MemInfoDTO extends BaseDTO implements UserDetails {

    /**
     * 会员id
     */
    @TableId
    @ApiModelProperty(name = "memId", value = "memId", notes = "会员ID", dataType = "Long")
    private Long memId;

    /**
     * 会员账号
     */
    @TableField(value = "mem_account")
    @NotEmpty(message = "会员账号不能为空")
    @ApiModelProperty(name="memAccount", value = "memAccount", notes = "会员账号", dataType = "String")
    private String memAccount;

    /**
     * 会员密码
     */
    @TableField(value = "mem_pwd")
    @NotEmpty(message = "会员密码不能为空")
    @ApiModelProperty(name="memPwd", value = "memPwd", notes = "加密后的会员密码", dataType = "String")
    private String memPwd;

    /**
     * 会员类型:1-vip;2-代理
     */
    @TableField(value = "mem_type")
    @NotEmpty(message = "会员类型不能为空")
    @ApiModelProperty(name="memType", value = "memType", notes = "会员类型", dataType = "Integer", example = "1", allowableValues = "1,2")
    private Integer memType;

    /**
     * 会员信用额度,单位分
     */
    @TableField(value = "total_credit_amount")
    @NotEmpty(message = "会员信用额度不能为空")
    @ApiModelProperty(name = "totalCreditAmount", value = "totalCreditAmount", notes = "会员总信用额度,单位分", dataType = "Long", example = "10000")
    private Long totalCreditAmount;

    /**
     * 会员已使用信用额度,单位分
     */
    @ApiModelProperty(name = "usedCreditAmount", value = "usedCreditAmount", notes = "会员已使用信用额度,单位分", dataType = "Long", example = "5000")
    @TableField(value = "used_credit_amount")
    private Long usedCreditAmount;

    @TableField(exist = false)
    private List<GrantedAuthority> authorities = new ArrayList<>();

    @Override
    public Collection<GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return this.memPwd;
    }

    @Override
    public String getUsername() {
        return this.memAccount;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

新建JwtToken认证过滤器

public class JwtAuthenticationFilterBean extends GenericFilterBean {

    private final static Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilterBean.class);

    private String AUTHORIZATION_NAME = "Authorization";

    private String BEARER = "Bearer";

    private static List<String> whiteRequestList = new ArrayList<>();

    static {
        whiteRequestList.add("/bonus/member/checkSafetyCode");
        whiteRequestList.add("/bonus/login");
        whiteRequestList.add("/bonus/member/login");
        whiteRequestList.add("/bonus/common/kaptcha");
        whiteRequestList.add("/bonus/admin/login");
        whiteRequestList.add("/bonus/favicon.ico");
        whiteRequestList.add("/bonus/doc.html");
        whiteRequestList.add("/bonus/error");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        logger.info("requestUrl="+request.getRequestURI());
        if(whiteRequestList.contains(request.getRequestURI()) || (request.getRequestURI().contains("admin/dist") &&
                request.getRequestURI().endsWith(".css") || request.getRequestURI().equals(".js") ||
                request.getRequestURI().endsWith(".png") || request.getRequestURI().endsWith("favicon.ico"))){
            // 如果是登录和安全码验证请求直接放行
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        } else {
               String bearerToken = request.getHeader(AUTHORIZATION_NAME);
               if(StringUtils.isEmpty(bearerToken)||!bearerToken.startsWith(BEARER)){
                   printException(response, HttpStatus.UNAUTHORIZED.value(), "缺失jwt令牌或令牌格式错误");
                   return;
               }
               String authToken = bearerToken.substring(bearerToken.indexOf(BEARER)+BEARER.length()+1);
               if(StringUtils.isEmpty(authToken)){
                   String message = "http header Authorization is null, user Unauthorized";
                   response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                   response.setStatus(HttpStatus.UNAUTHORIZED.value());
                   this.printException(response, HttpStatus.UNAUTHORIZED.value(), message);
                   return;
               } else {
                   try {
                       DecodedJWT decodedJWT = JWT.decode(authToken);
                       Map<String, Claim> claimMap = decodedJWT.getClaims();
                       Claim expireClaim = claimMap.get("exp");
                       Date expireDate = expireClaim.asDate();
                       // 校验token 是否过期
                       if(expireDate.before(DateUtil.date(System.currentTimeMillis()))){
                           String message = "Authorization token expired";
                           this.printException(response, HttpStatus.UNAUTHORIZED.value(), message);
                           return;
                       }
                       Claim memAccountClaim = claimMap.get("memAccount");
                       if(memAccountClaim==null || StringUtils.isEmpty(memAccountClaim.asString())){
                           String message = "memAccount cannot be null";
                           response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                           response.setStatus(HttpStatus.UNAUTHORIZED.value());
                           this.printException(response, HttpStatus.UNAUTHORIZED.value(), message);
                           return;
                       }
                       // 请求头认证通过, 放行请求
                       filterChain.doFilter(servletRequest, servletResponse);
                   } catch (JWTDecodeException e) {
                       String message = "JWT decode authToken failed, caused by " + e.getMessage();
                       this.printException(response, HttpStatus.UNAUTHORIZED.value(), message);
                       return;
                   }
               }
        }

    }

    /**
     * 打印请求头认证失败信息
     * @param response
     * @param status
     * @param message
     * @throws IOException
     */
    private void printException(HttpServletResponse response, int status, String message) throws IOException {
        logger.error(message);
        response.setStatus(status);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        PrintWriter printWriter = response.getWriter();
        ResponseResult<String> responseResult = ResponseResult.error(status, message);
        printWriter.write(JSONObject.toJSONString(responseResult));
        printWriter.flush();
        printWriter.close();
    }
}

Spring Security配置类中配置登录成功后返回jwt 令牌

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final static Logger logger = LoggerFactory.getLogger(SecurityConfig.class);

    @Resource
    private MemInfoService memInfoService;

    private MathContext mathContext = new MathContext(2, RoundingMode.HALF_UP);

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
        auth.userDetailsService(memInfoService);
    }

    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/static/**","/index.html","/templates/**", "/admin/**", "/doc.html", "/webjars/**", "/v2/*", "/favicon.ico", "/swagger-resources");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        JwtAuthenticationFilterBean jwtAuthenticationFilterBean = new JwtAuthenticationFilterBean();
        http.addFilterBefore(jwtAuthenticationFilterBean, UsernamePasswordAuthenticationFilter.class); // 将JwtToken认证过滤器注册在登录认证过滤器之前
        // 配置跨域
        http.cors().configurationSource(corsConfigurationSource())
                .and().logout().invalidateHttpSession(true).logoutUrl("/member/logout").permitAll()
        ;
        http.authorizeRequests().antMatchers("/member/checkSafetyCode").permitAll()
                .antMatchers("/doc.html").permitAll()
                .antMatchers("/common/kaptcha").permitAll()
                .antMatchers("/admin/login").permitAll()
                .anyRequest().authenticated()
                .and().httpBasic()
                .and().formLogin()
                .loginProcessingUrl("/member/login") // 登录接口
                .successHandler((httpServletRequest, httpServletResponse, authentication) -> {
                     httpServletResponse.setContentType("application/json;charset=utf-8");
                     httpServletResponse.setStatus(HttpStatus.OK.value());
                     PrintWriter printWriter = httpServletResponse.getWriter();
                     MemInfoDTO memInfoDTO = (MemInfoDTO) authentication.getPrincipal();
                     Map<String, Object> userMap = new HashMap<>();
                     userMap.put("memId", memInfoDTO.getMemId());
                     userMap.put("memAccount", memInfoDTO.getMemAccount());
                     userMap.put("memPwd", memInfoDTO.getMemPwd());
                     BigDecimal totalCredit = memInfoDTO.getTotalCreditAmount()!=null?new BigDecimal(memInfoDTO.getTotalCreditAmount()/100, mathContext): new BigDecimal("0.0");
                     userMap.put("totalCreditAmount", totalCredit);
                     BigDecimal usedCredit = memInfoDTO.getUsedCreditAmount()!=null?new BigDecimal(memInfoDTO.getUsedCreditAmount()/100, mathContext):new BigDecimal("0.0");
                     userMap.put("usedCreditAmount", usedCredit);
                     Long remainCredit = (memInfoDTO.getTotalCreditAmount()==null?0:memInfoDTO.getTotalCreditAmount()) - (memInfoDTO.getUsedCreditAmount()==null?0:memInfoDTO.getUsedCreditAmount());
                     BigDecimal remainCreditAmount = new BigDecimal(remainCredit/100, mathContext);
                     userMap.put("remainCreditAmount", remainCreditAmount);
                     userMap.put("authorities", memInfoDTO.getAuthorities());
                     Map<String, Object> dataMap = new HashMap<>();
                     dataMap.put("memInfo", userMap);
                     dataMap.put("authenticatedToken", "Bearer "+JwtTokenUtil.genAuthenticatedToken(userMap));
                     ResponseResult<Map<String, Object>> responseResult = ResponseResult.success(dataMap, "login success");
                     printWriter.write(JSONObject.toJSONString(responseResult));
                     printWriter.flush();
                     printWriter.close();
                }).failureHandler((httpServletRequest, httpServletResponse, e) -> {
                     logger.error("login failed, caused by " + e.getMessage());
                     httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
                     httpServletResponse.setStatus(HttpStatus.OK.value());
                     PrintWriter printWriter = httpServletResponse.getWriter();
                     ResponseResult<String> responseResult = ResponseResult.error(HttpStatus.UNAUTHORIZED.value(), "authentication failed");
                     responseResult.setPath(httpServletRequest.getRequestURI());
                     printWriter.write(JSONObject.toJSONString(responseResult));
                     printWriter.flush();
                     printWriter.close();
                }).permitAll()
                .and().csrf().disable().exceptionHandling().accessDeniedHandler(accessDeniedHandler());

    }

    //配置跨域访问资源
    private CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source =   new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");	//同源配置,*表示任何请求都视为同源,若需指定ip和端口可以改为如“localhost:8080”,多个以“,”分隔;
        corsConfiguration.addAllowedHeader("*");//header,允许哪些header,本案中使用的是token,此处可将*替换为token;
        corsConfiguration.addAllowedMethod("*");	//允许的请求方法,PSOT、GET等
        corsConfiguration.setAllowCredentials(true);
        // 注册跨域配置
        source.registerCorsConfiguration("/**",corsConfiguration); //配置允许跨域访问的url
        return source;
    }

    @Bean
    AccessDeniedHandler accessDeniedHandler() {
        return new AuthenticationAccessDeniedHandler();
    }
}

测试效果

在启动类中运行Main方法运行服务后就可以测试效果了

测试生成jwt令牌

我们首先测试生成jwt token的登录接口, 在postman中调用登录接口

post http://localhost:8090/bonus/member/login??username=zhangsan&password=zhangsan1234

接口返回信息如下:

{
    "code": 200,
    "data": {
        "memInfo": {
            "memAccount": "zhangsan",
            "totalCreditAmount": 2000,
            "memPwd": "82dea760d7bb362ca74883836ee4d6ba",
            "remainCreditAmount": 2000,
            "usedCreditAmount": 0,
            "authorities": [
                {
                    "authority": "ROLE_USER"
                }
            ],
            "memId": 1592927262097924097
        },
        "authenticatedToken": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZW1BY2NvdW50IjoiemhhbmdzYW4iLCJ0b3RhbENyZWRpdEFtb3VudCI6MjAwMC4wLCJtZW1Qd2QiOiI4MmRlYTc2MGQ3YmIzNjJjYTc0ODgzODM2ZWU0ZDZiYSIsInJlbWFpbkNyZWRpdEFtb3VudCI6MjAwMC4wLCJ1c2VkQ3JlZGl0QW1vdW50IjowLjAsImV4cCI6MTY3MjU1ODAyMSwiaWF0IjoxNjcxOTUzMjIxLCJqdGkiOiI2M2M1YmExZDIzZGY0YjIzODQ1NWU5YjkwNzQzMzRmMSIsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJtZW1JZCI6MTU5MjkyNzI2MjA5NzkyNDA5N30.S5UQLasL-SALKBHwhhUk_DGv__YPlRJQ7TC1pBzxb0g"
    },
    "message": "login success"
}

memPwd字段为密码加密后的密文

authenticatedToken 对应的内容为Bearer模式的jwt令牌, 真正的jwt令牌内容为eyj开头的那串较长的字符串。

测试通过jwt令牌认证与鉴权

新建一个获取配置数据的接口

@RestController
@RequestMapping("/config")
public class ConfigController {

    @Resource
    private ZodiacProperties zodiacProperties;

    @GetMapping("/twelve/zodiacs")
    public ResponseResult<ZodiacProperties> getTwelveZodiacs(){

        return ResponseResult.success(zodiacProperties);
    }

}

ZodiacProperties类源码如下:

@Component
@ConfigurationProperties(prefix = "twelve.zodiac")
public class ZodiacProperties {

    private String mouse;

    private String cow;

    private String tiger;

    private String rabbit;

    private String dragon;

    private String snake;

    private String horse;

    private String sheep;

    private String monkey;

    private String chicken;

    private String dog;

    private String pig;
    // 省略set、get方法
}

接口写好后,重启后台服务,并重新登录拿到jwt令牌令牌

首先试一下不在请求头中加入jwt令牌的结果

GET http://localhost:8090/bonus/config/twelve/zodiacs

接口返回结果:

{
    "code": 401,
    "message": "缺失jwt令牌或令牌格式错误"
}

然后在请求头中加入Authentication参数jwt令牌再次测试结果:
api

此时返回结果:

{
    "code": 200,
    "message": "ok",
    "path": null,
    "data": {
        "mouse": "03,15,27,39",
        "cow": "02,14,26,38",
        "tiger": "01,13,25,37,49",
        "rabbit": "12,24,36,48",
        "dragon": "11,23,35,47",
        "snake": "10,22,34,46",
        "horse": "09,21,33,45",
        "sheep": "08,20,32,44",
        "monkey": "07,19,31,43",
        "chicken": "06,18,30,42",
        "dog": "05,17,29,41",
        "pig": "04,16,28,40"
    }
}

关于如何在集成spring security安全访问框架的spring boot项目中如何使用jwt令牌安全访问服务端API就讲到这里,需要项目源码的读者朋友关注笔者的微信公众号【阿福谈Web编程】,并发送关键字信息【bonus-backend】即可获得项目源码下载地址。

参考文章

[1]: JWT token 介绍

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/114917.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

五、传输层(二)UDP

目录 2.1 UDP概述 2.2 UDP的首部格式 2.3 UDP校验 2.1 UDP概述 UDP无须建立连接。因此不会引入建立连接的时延。 UDP为无连接状态。因此当服务器使用UDP时&#xff0c;一般能支持更多的活动客户机。 UDP分组首部仅有8B的开销&#xff0c;而TCP有20B的首部开销。 应用层…

后端思维篇:如何抽个上报模板

前言 大家好&#xff0c;我是田螺。 我的后端思维专栏好久没更新啦&#xff0c;本文是后端思维专栏的第六篇哈。我的整个后端思维专栏都是跟日常工作相关的哈。 最近刚好优化了安全上报这块的代码&#xff0c;抽了一个基础模板&#xff0c;看起来挺优雅的。所以今天手把手教…

〖产品思维训练白宝书 - 产品思维认知篇⑤〗- 学习 [产品思维] 需要做哪些准备?

大家好&#xff0c;我是 哈士奇 &#xff0c;一位工作了十年的"技术混子"&#xff0c; 致力于为开发者赋能的UP主, 目前正在运营着 TFS_CLUB社区。 &#x1f4ac; 人生格言&#xff1a;优于别人,并不高贵,真正的高贵应该是优于过去的自己。&#x1f4ac; &#x1f4e…

php wampserver的使用配置

php wampserver的使用配置wampserver1.php时区配置2.修改apache服务器端口号3.设置起始页4.设置web服务器主目录5.设置虚拟目录wampserver WampServer是Windows Apache Mysql PHP集成安装环境&#xff0c;在Windows操作系统下的apache、php和mysql的服务器软件。 1.php时区配…

RabbitMQ 第二天 高级 7 RabbitMQ 高级特性 7.2 Consumer Ack

RabbitMQ 【黑马程序员RabbitMQ全套教程&#xff0c;rabbitmq消息中间件到实战】 文章目录RabbitMQ第二天 高级7 RabbitMQ 高级特性7.2 Consumer Ack7.2.1 Consumer Ack7.2.2 Consumer Ack 小结7.2.3 消息可靠性总结第二天 高级 7 RabbitMQ 高级特性 7.2 Consumer Ack 7.2.…

12.25日周报

周报 代码行数&#xff1a; 周一 704 周二 481 周三 571 周四 589 周五 595 周六 520 周日 537 遇到的问题&#xff1a; 没用过的方法AtomicInteger Insert Proto currentTimeMillis RequestParam BufferedReader UriComponents RestTemplate OSS 不清楚在…

公众号开发(2) —— 盛派.net SDK + vue搭建微信公众号网页开发框架

需求&#xff1a;通过微信公众号菜单跳转到手机端网页&#xff0c;跳转后通过微信授权登录获取微信公众号用户的OpenId&#xff08;用户关注公众号后&#xff0c;用户在公众号的唯一凭证&#xff09;&#xff0c;通过OpenId和后台数据库用户信息绑定起来并实现一些业务逻辑。 技…

基于51单片机的电子闹钟设计

使用的单片机是 STC89C52 此设计可以 年 月 日 时 分 秒显示和闹钟功能 能通过8个按键自由调整 时 分 秒 闹钟响铃时间 带复位按键&#xff0c;要是模块抽风&#xff0c;摁复位按键即可&#xff01; 使用 LCD16020A 屏幕显示 屏幕电路设有电位器&#xff…

Tableau可视化设计案例-07 多边形地图和背景图地图

Tableau可视化设计案例 本文是Tableau的案例&#xff0c;为B站视频的笔记&#xff0c;B站视频 参考&#xff1a;https://www.bilibili.com/video/BV1E4411B7ef 参考&#xff1a;https://blog.csdn.net/lianjiabin/category_9826951.html 数据下载地址为&#xff1a;https://do…

Java项目:springboot药品管理系统

作者主页&#xff1a;源码空间站2022 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 项目介绍 本项目属于前后端分离的项目&#xff0c;分为两个角色药品管理员和取药处人员 药品管理员&#xff1a; 登录、退出、药品信息录入、药厂信息录入…

Huawei Certified ICT Professional work (一)

文章目录一&#xff0c; 要求二&#xff0c;搭建拓扑图三&#xff0c;配置接口IP和环回IP四&#xff0c;进行RIP版本的配置并且宣告网段五&#xff0c;实现不同版本的连通&#xff0c;在交界处配置对端的版本号六&#xff0c;R3访问R7的环回地址走R5&#xff0c;改变R4的度量值…

Java项目:SpringBoot+MyBatis送水公司管理系统

作者主页&#xff1a;源码空间站2022 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 项目介绍 这个项目是一个基于SpringBootMyBatis的送水公司管理系统 管理员权限包括&#xff1a; 客户管理 送水工管理 送水历史管理 计算工资 统计送水数…

79页智慧应急指挥平台1 6 N体系建设方案

【版权声明】本资料来源网络&#xff0c;仅用于行业知识分享&#xff0c;供个人学习参考&#xff0c;请勿商用。【侵删致歉】如有侵权请联系小编&#xff0c;将在收到信息后第一时间进行删除&#xff01; 完整资料领取见文末&#xff0c;部分资料内容&#xff1a; 行业专网解决…

ARM_SMMU_下

SMMU驱动代码分析 本文主要分析linux kernel中SMMUv3的代码(drivers/iommu/arm-smmu-v3.c) linux kernel版本是linux 5.7, 体系结构是aarch64 SMMU的作用是把CPU提交给设备的VA地址&#xff0c;直接作为设备发出的地址&#xff0c;变成正确的物理地址&#xff0c;访问到物理内…

五、传输层(一)传输层的功能

目录 1.1传输层的主要功能 1.2传输层的寻址与端口 1.2.1端口的作用 1.2.2端口号 1.2.3套接字 1.3无连接服务与面向连接服务 1.1传输层的主要功能 物理层、数据链路层和网络层共同解决了主机通过异构网络互联起来所面临的问题&#xff0c;实现了主机到主机的通信。然而在…

【iOS】CAlayer的认识与使用

什么是CALayer CALayer是UIView里的一个图层&#xff0c;其主要功能是负责显示视图与动画。CALayer和UIView 功能是一致的、 不过因为其 更加底层 所以 CALayer 有一些接口、 UIView 里面没有。 CALayer与UIView UIView&#xff1a;用于管理视图的容器。每次创建UIView对象时…

当我阳了之后是如何用Python来自动买药的

人生苦短&#xff0c;我用Python序言准备工作代码实战序言 哈喽兄弟们&#xff0c;我是郑再阳&#xff0c;马上要成杨过了&#xff01; 读者&#xff1a;在下羊了个羊&#xff01; 最近总是听说哪里哪里阳了&#xff0c;哪个公司又团灭了&#xff0c;emmm~ 于是乎看了几天后…

【exgcd】扩展欧几里得

主要介绍扩展欧几里的和总结一些常用性质 首先介绍裴蜀定理 对于任意整数a,b,存在一对整数x,y 满足 axbygcd(a,b) 即存在x0,y0使得ax0by0gcd(a,b) 扩展欧几里得可以求出x0,y0 从而当axbyc 可以求出其通解 设dgcd(a,b) 显然当c%d!0时无整数解 通解可以表示为 x(c/d)x0k…

Java项目:springboot访客管理系统

作者主页&#xff1a;源码空间站2022 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 项目介绍 springboot搭建的访客管理系统&#xff0c;针对高端基地做严格把控来访人员信息管理&#xff0c;用户后端可以设置多个管理员帐号&#xff0c;给…

力扣(LeetCode)207. 课程表(C++)

拓扑排序 根据示例看出&#xff0c;课程表是否存在环&#xff0c;是问题的关键。这题的环&#xff0c;和数组、链表的环不一样&#xff0c;不好判&#xff0c;要转化成图判拓扑序列。 考虑向右和向左的方向&#xff0c;拓扑序列的所有边可以指向同一方向。 无环图进行重排序…