基于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";
}
文章评论