Spring Security的项目中集成JWT Token令牌安全访问后台API
引言
最近接了一个私活项目,后台使用的是Spring Boot脚手架搭建的,认证和鉴权框架用的Spring Security。同时为了确保客户端安全访问后台服务的API,需要用户登录成功之后返回一个包含登录用户信息的jwt token, 用于调用其他接口时将此jwt token携带在请求头中作为调用者的认证信息。最近一个多月一方面在忙着做这个项目,另一方面恰好遇上了精彩的世界杯,也没怎么发文了。很多时候真的深感写篇原创文章比单纯的敲代码麻烦多了,但是好久不更文还是要检讨一下自己的惰性,客服自身的惰性是每个想要突破自我、不甘平庸的普通人的一辈子都不能松懈的重任。
JWT 简介
首先,让我们来补一下jwt的知识。jwt token
的全称叫JSON Web Token
,主要用于在各方之间以JSON 对象方式安全地传输信息。此信息是数字签名的,可以验证和信任,JWT 可以使用密钥(使用 HMAC
算法)或使用 RSA
或 ECDSA
的公钥/私钥对进行签名。
虽然 JWT 可以加密以在各方之间提供保密性,但我们将专注于签名令牌。签名的令牌可以验证其中包含的声明的完整性,而加密的令牌会向其他方隐藏这些声明。当使用公钥/私钥对对令牌进行签名时,只有持有私钥的一方才可以签署。
jwt token 的适用场景
-
鉴权(Authorization):这是最常见的场景。用户登录后,每个后续请求都将包含 JWT,从而允许用户访问该令牌允许的路由、服务和资源。 单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小并且能够在不同的域中轻松使用。
-
信息交换(Information Exchange):JWT令牌是在各方之间安全传输信息的好方法。 因为可以对 JWT 进行签名(例如,使用公钥/私钥对),所以可以确定发件人就是他们所说的那个人。此外,由于使用
header
和payload
计算签名,还可以验证内容是否被篡改。
jwt 的结构
JWT 由header
、payload
和signature
三部分组成,以 . 分割:
-
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 具有前面介绍过的header和payload编码,并使用密钥签名
我们可以在 jwt.io Debugger 网站来解码、验证和生成 JWT。
jwt 的使用方式
在身份校验中,当用户成功登录,将返回一个 JSON Web Token
。由于令牌是凭据,因此必须非常小心以防止出现安全问题。
通常令牌需要设置一个过期时间,超过过期时间则令牌失效,需要置换新的令牌。
由于缺乏安全性,不应该将敏感的会话数据存储在浏览器中。每当用户需要访问受保护的路由或资源时,用户代理应该发送jwt,通常在 Authorization header 中使用 Bearer 模式。 header 的内容应如下所示:
Authorization: Bearer <token>
某些情况下,这可以是一种无状态授权机制。服务器的受保护路由将检查 Authorization header 中是否存在有效的 JWT,如果存在,则允许用户访问受保护的资源。如果 JWT 包含必要的数据,则可能会减少查询数据库以进行某些操作的需要,尽管情况并非总是如此。
如果 token
在 Authorization header
,跨域 Cross-Origin Resource Sharing (CORS)
不是问题,因为它不使用 cookies
。
客户端获取jwt令牌访问受保护资源的具体流程
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.JWT
和com.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
主要用于构造header
和payload
中 的内容, 该静态类主要提供一些列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令牌再次测试结果:
此时返回结果:
{
"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 介绍