Spring Boot实战之Filter实现使用JWT进行接口认证

2018-02-05 1808点热度 1人点赞 0条评论

基于JWT(Json Web Token)的授权方式

JWT 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权;

从客户端请求服务器获取token, 用该token 去访问实现了jwt认证的web服务器。 token 可保存自定义信息,如用户基本信息, web服务器用key去解析token,就获取到请求用户的信息了;
很多时候,web服务器和授权服务器是同一个项目,所以也可以用以下架构:

参考文档(可看可不看):
开箱即用 - jwt 无状态分布式授权 引用了里边的几张图
什么是 JWT -- JSON WEB TOKEN 介绍了什么是JWT
讲真,别再使用JWT了! 介绍了JWT的优缺点及使用场景

在接口中使用JWT干什么?

由于服务是开放的restful服务,谁都可以调用,因此需要一种认证方式去确认调用者的身份,在服务当中,调用者一般为各个应用系统,因此只需要校验应用系统的身份即可。在接口服务当中,内置(写死)了一些应用编码和密码,调试和上线的时候需要和应用系统进行商定。由于要传输密码,属于敏感信息,所以使用了AES对称加密对应用编码和应用密码进行加密(com.ai.anhui.faservice.util.AESUtil中有加密、解密方法,秘钥配置(写死)在com.ai.anhui.faservice.domain.FAserviceConstants中)。把加密后的应用编码和应用密码给应用即可。

使用maven创建spring boot工程

创建maven工程

直接创建maven工程或者首先创建java工程然后转成maven工程均可,最后添加spring boot所需依赖即可,完整pom.xml如下:

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.ai.anhui</groupId>
    <artifactId>faservice</artifactId>
    <version>0.0.1</version>
    <name>faservice</name>
    <description>服务</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.10.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/junit/junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.6</version>
        </dependency>

        <dependency>
            <groupId>com.oracle</groupId>
            <artifactId>ojdbc6</artifactId>
            <version>11.2.0.3</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/log4j/log4j -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

说明:
1. 此处使用了spring boot版本为1.5.10.RELEASE
2. ORM框架使用mybatis
3. 日志使用log4j
4. 数据源使用druid
5. 数据库驱动使用ojdbc6
6. 测试框架使用junit

项目目录结构图


目录放置文件说明:
1. auth:放置JWT认证相关类
2. controller:JWT认证接口和服务开放接口
3. db:数据库相关
4. domain:实体类
5. util:工具类
6. test/java 测试类
7. src/main/resource资源文件及配置文件
8. Main.java spring boot程序入口

服务相关配置application.properties

spring boot的默认配置文件,可以配置程序的启动端口

#server.contextPath=/guide
server.port=18002

日志相关配置

#set logging level
logging.level.=error
logging.level.com.ai.anhui=DEBUG
logging.level.java.sql.Connection=DEBUG
logging.level.java.sql.ResultSet=DEBUG
logging.level.java.sql.Statement=DEBUG
logging.level.java.sql.PreparedStatement=DEBUG
logging.file=./log/faService.log

spring配置applicationContext.xml

主要配置数据源,集成mybatis,事务管理,配置文件如下:

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
            http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
           http://www.springframework.org/schema/context 
           http://www.springframework.org/schema/context/spring-context-4.0.xsd
           http://www.springframework.org/schema/aop 
           http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
           http://www.springframework.org/schema/tx 
           http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">
    <!-- 引入properties文件 -->
    <context:property-placeholder location="classpath*:/dataSource.properties" />

    <bean name="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
        init-method="init" destroy-method="close">
        <property name="url" value="${dataSource.url}" />
        <property name="username" value="${dataSource.username}" />
        <property name="password" value="${dataSource.password}" />
        <!-- 初始化连接大小 -->
        <property name="initialSize" value="0" />
        <!-- 连接池最大使用连接数量 -->
        <property name="maxActive" value="20" />
        <!-- 获取连接最大等待时间 -->
        <property name="maxWait" value="60000" />
        <property name="validationQuery" value="${validationQuery}" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <property name="testWhileIdle" value="true" />
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="25200000" />
        <!-- 打开removeAbandoned功能 -->
        <property name="removeAbandoned" value="true" />
        <!-- 1800秒,也就是30分钟 -->
        <property name="removeAbandonedTimeout" value="1800" />
        <!-- 关闭abanded连接时输出错误日志 -->
        <property name="logAbandoned" value="true" />
        <!-- 监控数据库 -->
        <!-- <property name="filters" value="stat" /> -->
        <property name="filters" value="mergeStat" />
    </bean>
    <!-- ========================================针对myBatis的配置项============================== -->
    <!-- 配置sqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- 实例化sqlSessionFactory时需要使用上述配置好的数据源以及SQL映射文件 -->
        <property name="dataSource" ref="dataSource" />
        <!-- 自动扫描com/ai/anhui/faservice/db/目录下的所有SQL映射的xml文件,
         省掉Configuration.xml里的手工配置 value="classpath:com/ai/anhui/faservice/db/*.xml"
         指的是classpath(类路径)下com/ai/anhui/faservice/db/包中的所有xml文件 
        FAServiceSqlMap.xml位于com/ai/anhui/faservice/db/包下,这样FAServiceSqlMap.xml就可以被自动扫描 -->

        <property name="mapperLocations" value="classpath:com/ai/anhui/faservice/db/*.xml" />
    </bean>
    <!-- 配置扫描器 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- 扫描me.gacl.dao这个包以及它的子包下的所有映射接口类 -->
        <property name="basePackage" value="com.ai.anhui" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
    </bean>
    <!-- ========================================分隔线========================================= -->
    <!-- 配置Spring的事务管理器 -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!-- 注解方式配置事物 -->
    <tx:annotation-driven transaction-manager="transactionManager" />

    <!-- 配置druid监控spring jdbc -->
    <bean id="druid-stat-interceptor"
        class="com.alibaba.druid.support.spring.stat.DruidStatInterceptor">
    </bean>
    <bean id="druid-stat-pointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut"
        scope="prototype">
        <property name="patterns">
            <list>
                <value>com.ai.anhui.faservice.service.*</value>
            </list>
        </property>
    </bean>
    <aop:config>
        <aop:advisor advice-ref="druid-stat-interceptor"
            pointcut-ref="druid-stat-pointcut" />
    </aop:config>
</beans>

数据源配置文件dataSource.properties

配置数据库连接参数

#3a dataSource
dataSource.url=jdbc:oracle:thin:@127.0.0.1:20000:lsorcl
dataSource.username=linkage
dataSource.password=***************
validationQuery=select 1 from dual

.log.ip=10.21.16.51
.log.port=5140
.log.logType=oldType
cas.log.ip=10.21.16.51
cas.log.port=5160

log4j配置文件log4j.properties

### log4j settings

## loggers definition
log4j.rootLogger=debug, stdout
log4j.logger.com.ai.anhui=debug, console, stdout

## standard output logger
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[console] %d -%-4r [%t] %-5p %l %x - %m%n

## file logger for errors
log4j.appender.errors=org.apache.log4j.DailyRollingFileAppender
log4j.appender.errors.File=${server.root}/log/errors.log
log4j.appender.errors.DatePattern='.'yyyy-MM-dd
log4j.appender.errors.layout=org.apache.log4j.PatternLayout
log4j.appender.errors.layout.ConversionPattern=[console] %d -%-4r [%t] %-5p %l %x - %m%n

## file logger for com.linkage.fa.console
log4j.appender.console=org.apache.log4j.DailyRollingFileAppender
log4j.appender.console.File=${server.root}/log/console.log
log4j.appender.console.DatePattern='.'yyyy-MM-dd
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=[console] %d -%-4r [%t] %-5p %l %x - %m%n

mybatis使用手动写xml的方式sqlMapConfig.xml

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE sqlMapConfig PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
    "http://ibatis.apache.org/dtd/sql-map-config-2.dtd">

<sqlMapConfig>
    <settings useStatementNamespaces="true" />
    <sqlMap resource="com/ai/anhui/faservice/db/FAServiceSqlMap.xml" />
</sqlMapConfig>

FAServiceSqlMap.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- namespace命名空间,作用就是对sql进行分类化管理,即sql隔离 注意:使用mapper代理方法开发的话,namespace就有特殊重要的作用了 -->
<mapper namespace="FAService">

</mapper>

spring boot程序入口Main.java

/**
 * 
 */
package com.ai.anhui.faservice;

import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ImportResource;

import com.ai.anhui.faservice.auth.FAServiceAuthorFilter;

/**
 * 启动程序
 * 
 * @author WangXianfeng 2018年2月1日 下午5:23:43
 */
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class,
        DataSourceTransactionManagerAutoConfiguration.class })
@ImportResource({ "classpath*:applicationContext.xml" })
public class Main {
    private static Logger logger = Logger.getLogger(Main.class);

    public static void main(String[] args) {
        logger.info("欢迎进入安徽移动服务程序,开始启动……");
        SpringApplication.run(Main.class, args);
        logger.info("欢迎使用安徽移动服务程序,启动完成");
    }
    /**
     * 注册验证应用编码和应用密码的过滤器,只对/faService/*下的请求进行验证
     * @return
     * @author WangXianfeng 2018年2月5日 下午4:59:39
     */
    @Bean
    public FilterRegistrationBean basicFilterRegistrationBean() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        FAServiceAuthorFilter filter = new FAServiceAuthorFilter();
        registrationBean.setFilter(filter);
        List<String> urlPatterns = new ArrayList<String>();
        urlPatterns.add("/faService/*");
        registrationBean.setUrlPatterns(urlPatterns);
        return registrationBean;
    }
}

实现JWT

获取接口令牌接口JsonWebTokenController

接口的请求URL为http://ip:port/oauth/token
Headers:Content-Type:application/json
请求参数:

字段名称 数据类型 长度 说明
appCode String 256 应用编码,AES加密后的字符串,明文如:OA,联调的时候由与应用约定
appPwd String 256 应用密码,AES加密后的字符串,明文如:1234234,联调的时候由与应用约定

请求参数格式如下:
Content-Type:application/json

{
    "appCode": "NEY3QjNCOTZBRkY4QUQxRDZERUE5QjYyMTVGRTM4QjM=",
    "appPwd": "MDJBOTk4Njg0RjUxOTFBQzNBQzMyNDMyNzBCRTUwQzg3NEY5NjA4QTFCQjE1REY2MEFCQjgzNDI4OTNCRUVCMg=="
}

返回参数:

字段名称 数据类型 长度 说明
resultCode String 16 1000:获取Token成功 1001:应用编码或者密码错误
resultMsg String 512 结果描述。详细如上
data 返回信息容器
accessToken String 512 JWT令牌
tokenType String 16 令牌类型,暂固定为Bearer
expiresIn Int 10 令牌超时时间,单位:秒

返回参数格式如下:

{
    "resultCode": "1000",
    "resultMsg": "获取Token成功",
    "data": {
        "accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhcHBDb2RlIjoiTkVZM1FqTkNPVFpCUmtZNFFVUXhSRFpFUlVFNVFqWXlNVFZHUlRNNFFqTT0iLCJhcHBQd2QiOiJNREpCT1RrNE5qZzBSalV4T1RGQlF6TkJRek15TkRNeU56QkNSVFV3UXpnM05FWTVOakE0UVRGQ1FqRTFSRVkyTUVGQ1FqZ3pOREk0T1ROQ1JVVkNNZz09IiwiZXhwIjoxNTE3Nzk2MTgwLCJuYmYiOjE1MTc3OTU4ODB9.-6P04EY107gE4fukjstPINq9FeaX6YUme3CxbJ2_RA0",
        "tokenType": "Bearer",
        "expiresIn": 30
    }
}

JsonWebTokenController源码
逻辑说明:
从请求当中获取appCode和appPwd,首先进行判空,然后由validAppCodeAndPwd方法进行应用代码和密码的验证,验证不通过返回错误信息,验证通过,则通过JwtHelper的createJWT方法创建令牌。令牌信息由AccessToken进行封装。

/**
 * 
 */
package com.ai.anhui.faservice.controller;

import org.apache.log4j.Logger;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.ai.anhui.faservice.auth.AccessToken;
import com.ai.anhui.faservice.auth.JwtHelper;
import com.ai.anhui.faservice.domain.AppCodeAndPwd;
import com.ai.anhui.faservice.domain.AppInfo;
import com.ai.anhui.faservice.domain.FAserviceConstants;
import com.ai.anhui.faservice.domain.ResultMsg;
import com.ai.anhui.faservice.domain.ResultStatusCode;
import com.ai.anhui.faservice.util.AESUtil;

/**
 * 验证appCode和appPwd,获取token
 * @author WangXianfeng 2018年2月2日 下午3:48:18
 */
@RestController
public class JsonWebTokenController {
    private static Logger logger = Logger.getLogger(JsonWebTokenController.class);
    /**
     * 
     * @param appInfo
     * @return
     * @author WangXianfeng 2018年2月2日 下午5:19:37
     */
    @RequestMapping("/oauth/token")
    public Object getAccessToken(@RequestBody AppInfo appInfo) {
        ResultMsg resultMsg;
        try {
            if (appInfo.getAppCode() == null
                    || appInfo.getAppPwd() == null) {
                resultMsg = new ResultMsg(ResultStatusCode.INVALID_APP.getResultCode(),
                        ResultStatusCode.INVALID_APP.getResultMsg());
                return resultMsg;
            }

            if(null != appInfo && validAppCodeAndPwd(appInfo)) {
                // 拼装accessToken
                String accessToken = JwtHelper.createJWT(appInfo.getAppCode(), appInfo.getAppPwd(),
                        FAserviceConstants.JWT_EXPIRATION_TIME * 1000, FAserviceConstants.BASE64_CODE_KEY);
                // 返回accessToken
                AccessToken accessTokenEntity = new AccessToken();
                accessTokenEntity.setAccessToken(accessToken);
                accessTokenEntity.setExpiresIn(FAserviceConstants.JWT_EXPIRATION_TIME);
                accessTokenEntity.setTokenType(FAserviceConstants.BEARER_TOKEN_TYPE);
                logger.debug("获取Token成功:" + accessToken);
                resultMsg = new ResultMsg(ResultStatusCode.GET_TOKEN_SUCCESS.getResultCode(),
                        ResultStatusCode.GET_TOKEN_SUCCESS.getResultMsg(),accessTokenEntity);
            }else {
                resultMsg = new ResultMsg(ResultStatusCode.INVALID_APP.getResultCode(),
                        ResultStatusCode.INVALID_APP.getResultMsg());
            }
            return resultMsg;
        } catch (Exception ex) {
            logger.error("获取token的时候发生异常",ex);
            resultMsg = new ResultMsg(ResultStatusCode.SYSTEM_EXCEPTION.getResultCode(),
                    ResultStatusCode.SYSTEM_EXCEPTION.getResultMsg());
            return resultMsg;
        }
    }
    /**
     * 校验是否是合法的应用,即校验应用代码和应用密码是否与约定的匹配
     * @param appInfo
     * @return
     * @author WangXianfeng 2018年2月4日 上午11:52:23
     */
    private boolean validAppCodeAndPwd(AppInfo appInfo) {
        boolean validFlag = false;
        //解密请求参数
        String appCode = AESUtil.decrypt(FAserviceConstants.AES_TOKEN_KEY, appInfo.getAppCode());
        String appPwd = AESUtil.decrypt(FAserviceConstants.AES_TOKEN_KEY, appInfo.getAppPwd());
        logger.debug("appCode:" + AppCodeAndPwd.OA.getAppCode());
        logger.debug("@appPwd: " + AppCodeAndPwd.OA.getAppPwd());
        if(appCode.equals(AppCodeAndPwd.OA.getAppCode())&&appPwd.equals(AppCodeAndPwd.OA.getAppPwd())) {
            validFlag = true;
        }
        return validFlag;
    }
}

JWT实现

需要验证的信息:应用编码和应用密码AppInfo.java

其中只用到appCode和appPwd,由应用调用的时候传递过来

import java.io.Serializable;

/**
 * 应用系统信息
 * @author WangXianfeng 2018年2月2日 下午3:35:50
 */
public class AppInfo implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = -660238195043429159L;
    private String appCode;
    private String appName;
    private String appPwd;
    private String appDesc;
    ...
    getters and setters
}

有效的应用编码和密码封装在枚举类AppCodeAndPwd.java中,如果需要添加有效的应用编码及密码,需要修改此类,并且修改JsonWebTokenController中的validAppCodeAndPwd有效应用验证逻辑:

package com.ai.anhui.faservice.domain;

/**
 * 应用代码和密码枚举类
 * @author WangXianfeng 2018年2月4日 上午11:41:03
 */
public enum AppCodeAndPwd {
    OA("OA","ra(dF*XS3&y8^sGf%23b$pu");
    private String appCode;
    private String appPwd;
    public String getAppCode() {
        return appCode;
    }
    ……
}

JWT生成token及验证token工具类JwtHelper.java

/**
 * 
 */
package com.ai.anhui.faservice.auth;

import java.security.Key;
import java.util.Date;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;

import org.apache.log4j.Logger;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

/**
 * JWT生成token及验证token工具类
 * @author WangXianfeng 2018年2月2日 下午3:38:56
 */
public class JwtHelper {
    private static Logger logger = Logger.getLogger(JwtHelper.class);
    /**
     * 
     * @param jsonWebToken
     * @param base64Security
     * @return
     * @author WangXianfeng 2018年2月2日 下午3:40:49
     */
    public static Claims parseJWT(String jsonWebToken, String base64Security) {
        try {
            Claims claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(base64Security))
                    .parseClaimsJws(jsonWebToken).getBody();
            return claims;
        } catch (Exception ex) {
            logger.error("获取认证信息失败",ex);
            return null;
        }
    }
    /**
     * 生成JWT
     * @param appCode 应用编码,如OA
     * @param appPwd 应用密码
     * @param TTLMillis Token过期时间  
     * @param base64Security 
     * @return
     * @author WangXianfeng 2018年2月2日 下午3:44:47
     */
    public static String createJWT(String appCode, String appPwd,long TTLMillis,String base64Security)   
    {  
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;  

        long nowMillis = System.currentTimeMillis();  
        Date now = new Date(nowMillis);  

        //生成签名密钥  
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(base64Security);  
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());  

        //添加构成JWT的参数  
        JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")  
                                        .claim("appCode", appCode)
                                        .claim("appPwd", appPwd)
                                        .signWith(signatureAlgorithm, signingKey);  
        //添加Token过期时间  
        if (TTLMillis >= 0) {  
            long expMillis = nowMillis + TTLMillis;  
            Date exp = new Date(expMillis);  
            builder.setExpiration(exp).setNotBefore(now);
        }

        //生成JWT  
        return builder.compact();  
    } 
}

token返回结果类AccessToken.java

包含令牌、令牌类型和过期时间3个字段

字段名称 数据类型 长度 说明
accessToken String 512 JWT令牌
tokenType String 16 令牌类型,暂固定为Bearer
expiresIn Int 10 令牌超时时间,单位:秒
package com.ai.anhui.faservice.auth;

/**
 * token返回结果类
 * @author WangXianfeng 2018年2月2日 下午3:46:30
 */
public class AccessToken {
    private String accessToken;  
    private String tokenType;  
    private long expiresIn;
    ……
}

服务验证过滤器FAServiceAuthorFilter.java

过滤器从请求中获取http请求头中的Authorization,获取token值,使用JwtHelper的parseJWT方法进行校验,如果校验通过,则允许请求,如果不通过,则拒绝请求,报无效的令牌错误。

/**
 * 
 */
package com.ai.anhui.faservice.auth;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;

import com.ai.anhui.faservice.domain.FAserviceConstants;
import com.ai.anhui.faservice.domain.ResultMsg;
import com.ai.anhui.faservice.domain.ResultStatusCode;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * 服务验证过滤器
 * 
 * @author WangXianfeng 2018年2月2日 下午3:04:37
 */
public class FAServiceAuthorFilter implements Filter{
    private static Logger logger = Logger.getLogger(FAServiceAuthorFilter.class);
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // TODO Auto-generated method stub

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        ResultMsg resultMsg;  
        HttpServletRequest httpRequest = (HttpServletRequest)request;  
        String auth = httpRequest.getHeader("Authorization");  
        if ((auth != null) && (auth.length() > 7))  
        {  
            String headStr = auth.substring(0, 6);  
            if (headStr.equals(FAserviceConstants.BEARER_TOKEN_TYPE)){
                auth = auth.replace(FAserviceConstants.BEARER_TOKEN_TYPE + " ", "");
                if (JwtHelper.parseJWT(auth, FAserviceConstants.BASE64_CODE_KEY) != null){
                    logger.debug("@@@令牌校验成功,let's go!");
                    chain.doFilter(request, response);  
                    return;  
                }  
            }  
        }  

        HttpServletResponse httpResponse = (HttpServletResponse) response;  
        httpResponse.setCharacterEncoding("UTF-8");    
        httpResponse.setContentType("application/json; charset=utf-8");   
        httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);  

        ObjectMapper mapper = new ObjectMapper();  

        resultMsg = new ResultMsg(ResultStatusCode.INVALID_TOKEN.getResultCode(), 
                ResultStatusCode.INVALID_TOKEN.getResultMsg());  
        httpResponse.getWriter().write(mapper.writeValueAsString(resultMsg));  
        return;  
    }

    @Override
    public void destroy() {
        // TODO Auto-generated method stub

    }

}

封装返回结果ResultMsg.java

返回结果由ResultMsg返回,此类包含3个字段:

字段名称 数据类型 长度 说明
resultCode String 16 1000:获取Token成功 1001:应用编码或者密码错误
resultMsg String 512 结果描述。详细如上
data Object 返回信息容器

由于spring会自动把所有的javabean属性转换为json,所以此处data使用了一个Object类型,即可以封装所有的返回结果,以便共用。

package com.ai.anhui.faservice.domain;

import java.io.Serializable;

/**
 * 封装返回结果
 * @author WangXianfeng 2018年2月2日 下午3:57:11
 */
public class ResultMsg  implements Serializable{

    /**
     * 
     */
    private static final long serialVersionUID = -7968684299621884434L;
    private String resultCode;
    private String resultMsg;
    private Object data; 
    ……
}

结果代码和描述类ResultStatusCode.java

由于返回结果代码和描述较多,分散在各处不好管理,因此创建了一个枚举类型用于返回信息结果代码和描述的集中管理。

package com.ai.anhui.faservice.domain;
/**
 * 返回信息状态枚举
 * 
 * @author WangXianfeng 2018年2月4日 下午12:24:45
 */
public enum ResultStatusCode {
    GET_TOKEN_SUCCESS("1000","获取Token成功"),
    INVALID_APP("1001","应用编码或者密码错误"),
    INVALID_TOKEN("1003","无效的令牌"),
    PRACCT_BY_WORKNO("2000","根据工号查询主帐号成功"),
    NO_PRACCT_BY_WORKNO("2001",""),
    MANY_PRACCT_BY_WORKNO("2002",""),
    PRACCT_BY_MOBILE("2003",""),
    NO_PRACCT_BY_MOBILE("2004",""),
    MANY_PRACCT_BY_MOBILE("2005",""),
    SYSTEM_EXCEPTION("9999","系统异常");
    private String resultCode;
    private String resultMsg;
    private ResultStatusCode(String code,String msg) {
        this.resultCode = code;
        this.resultMsg = msg;
    }
    ……
}

程序入口处注册验证过滤器Main.java

URL注册为/faService/*,只对/faService/下的请求进行JWT验证,其他URL请求不做验证。

/**
     * 注册验证应用编码和应用密码的过滤器,只对/faService/*下的请求进行验证
     * @return
     * @author WangXianfeng 2018年2月5日 下午4:59:39
     */
    @Bean
    public FilterRegistrationBean basicFilterRegistrationBean() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        FAServiceAuthorFilter filter = new FAServiceAuthorFilter();
        registrationBean.setFilter(filter);
        List<String> urlPatterns = new ArrayList<String>();
        urlPatterns.add("/faService/*");
        registrationBean.setUrlPatterns(urlPatterns);
        return registrationBean;
    }

用到的AES对称加解密

package com.ai.anhui.faservice.util;

import java.security.SecureRandom;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.apache.log4j.Logger;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

/**
 * AES对称加密
 * 
 * @author WangXianfeng 2018年2月2日 下午5:42:46
 */
public class AESUtil {
    private static Logger logger = Logger.getLogger(AESUtil.class);
    private static final BASE64Encoder encoder = new BASE64Encoder();
    private static final BASE64Decoder decoder = new BASE64Decoder();
    /**
     * AES加密
     * 1.构造密钥生成器
     * 2.根据ecnodeRules规则初始化密钥生成器
     * 3.产生密钥
     * 4.创建和初始化密码器
     * 5.内容加密
     * 6.返回字符串
     * @param encodeRules 加密规则
     * @param content 加密内容
     * @return 返回加密后的字符串进行BASE64编码
     * @author WangXianfeng 2018年2月4日 上午8:14:48
     */
    public static String encrypt(String encodeRules, String content) {
        String encodeAES = null;
        try {
            // 1.构造密钥生成器,指定为AES算法,不区分大小写
            KeyGenerator keygen = KeyGenerator.getInstance("AES");
            // 2.根据ecnodeRules规则初始化密钥生成器
            // 生成一个128位的随机源,根据传入的字节数组
            keygen.init(128, new SecureRandom(encodeRules.getBytes()));
            // 3.产生原始对称密钥
            SecretKey original_key = keygen.generateKey();
            // 4.获得原始对称密钥的字节数组
            byte[] raw = original_key.getEncoded();
            // 5.根据字节数组生成AES密钥
            SecretKey key = new SecretKeySpec(raw, "AES");
            // 6.根据指定算法AES自成密码器
            Cipher cipher = Cipher.getInstance("AES");
            // 7.初始化密码器,第一个参数为加密(Encrypt_mode)或者解密解密(Decrypt_mode)操作,第二个参数为使用的KEY
            cipher.init(Cipher.ENCRYPT_MODE, key);
            // 8.获取加密内容的字节数组(这里要设置为utf-8)不然内容中如果有中文和英文混合中文就会解密为乱码
            byte[] byte_encode = content.getBytes("utf-8");
            // 9.根据密码器的初始化方式--加密:将数据加密
            byte[] byteAES = cipher.doFinal(byte_encode);
            // 10.将加密后的数据转换为字符串
            //首先把二进制转换成16进制,不转换解密的时候会报如下错误:
            //Input length must be multiple of 16 when decrypting with padded cipher
            //这主要是因为加密后的byte数组是不能强制转换成字符串的,换言之:字符串和byte数组在这种情况下不是互逆的;
            encodeAES = parseByte2HexStr(byteAES);
            // 这里用Base64Encoder中会找不到包
            // 解决办法:
            // 在项目的Build path中先移除JRE System Library,再添加库JRE System Library,重新编译后就一切正常了。
            encodeAES = encoder.encode(encodeAES.getBytes("utf-8"));
        } catch (Exception e) {
            logger.error("AES加密字符串发生错误",e);
        }
        // 如果有错就返加null
        return encodeAES;
    }
    /**
     * AES解密
     * 解密过程:
     * 1.同加密1-4步
     * 2.将加密后的字符串反纺成byte[]数组
     * 3.将加密内容解密
     * @param encodeRules
     * @param content
     * @return
     * @author WangXianfeng 2018年2月4日 上午8:19:25
     */
    public static String decrypt(String encodeRules,String content){
        String decodeAES = null;
        try {
            //1.构造密钥生成器,指定为AES算法,不区分大小写
            KeyGenerator keygen=KeyGenerator.getInstance("AES");
            //2.根据ecnodeRules规则初始化密钥生成器
            //生成一个128位的随机源,根据传入的字节数组
            keygen.init(128, new SecureRandom(encodeRules.getBytes()));
              //3.产生原始对称密钥
            SecretKey original_key=keygen.generateKey();
              //4.获得原始对称密钥的字节数组
            byte [] raw=original_key.getEncoded();
            //5.根据字节数组生成AES密钥
            SecretKey key=new SecretKeySpec(raw, "AES");
              //6.根据指定算法AES自成密码器
            Cipher cipher=Cipher.getInstance("AES");
              //7.初始化密码器,第一个参数为加密(Encrypt_mode)或者解密(Decrypt_mode)操作,第二个参数为使用的KEY
            cipher.init(Cipher.DECRYPT_MODE, key);
            //8.将加密并编码后的内容解码成字节数组
            //首先进行base64解码
            content = new String(decoder.decodeBuffer(content),"utf-8");;
            byte [] byte_content= parseHexStr2Byte(content);
            //解密
            byte [] byteDecode=cipher.doFinal(byte_content);
            decodeAES = new String(byteDecode,"utf-8");
        } catch (Exception e) {
            logger.error("AES解密字符串发生错误",e);
        }
        //如果有错就返加null
        return decodeAES;         
    }

    /**
     * 将二进制转换成16进制 
     * @param buf
     * @return
     * @author WangXianfeng 2018年2月4日 上午8:28:08
     */
    public static String parseByte2HexStr(byte buf[]){  
        StringBuffer sb = new StringBuffer();  
        for(int i = 0; i < buf.length; i++){  
            String hex = Integer.toHexString(buf[i] & 0xFF);  
            if (hex.length() == 1) {  
                hex = '0' + hex;  
            }  
            sb.append(hex.toUpperCase());  
        }  
        return sb.toString();  
    }  
    /**
     * 将16进制转换为二进制
     * @param hexStr
     * @return
     * @author WangXianfeng 2018年2月4日 上午8:28:15
     */
    public static byte[] parseHexStr2Byte(String hexStr){  
        if(hexStr.length() < 1)  
            return null;  
        byte[] result = new byte[hexStr.length()/2];  
        for (int i = 0;i< hexStr.length()/2; i++) {  
            int high = Integer.parseInt(hexStr.substring(i*2, i*2+1), 16);  
            int low = Integer.parseInt(hexStr.substring(i*2+1, i*2+2), 16);  
            result[i] = (byte) (high * 16 + low);  
        }  
        return result;  
    }
}

一些常量保存在FAserviceConstants.java中

/**
 * 
 */
package com.ai.anhui.faservice.domain;

/**
 * 系统中用到的一些常量
 * @author WangXianfeng 2018年2月4日 上午11:34:19
 */
public class FAserviceConstants {
    public final static String AES_TOKEN_KEY = "b!M9*u^4%3(V)H_G-G+l=o$K";
    public final static String BASE64_CODE_KEY = "QkRiMlJsSWpvaVRrVlpNMUZxVGtOUFZGcENVbXRaTkZGVlVYaFNSRnBGVWxWRk5WRnFXWGxOVkZaSFVsUk5ORkZxVFQwaUxDSmhjSA==";
    //JWT令牌过期时间,单位为秒
    public final static int JWT_EXPIRATION_TIME = 300;
    public final static String BEARER_TOKEN_TYPE = "Bearer";
}

参考文档:Spring Boot实战之Filter实现使用JWT进行接口认证

王显锋

激情工作,快乐生活!

文章评论