什么是JWT
实现原理
在security框架,增加自定义的jwt filter,通过继承OncePerRequestFilter,实现对每次请求的请求头进行处理。从请求头中获取 JWT Bearer tonken,对tonken进行解析和判定。
实现步骤
1 配置Maven
2 JWT相关工具类
3 定义Domain和Dto的数据结构
4 实现自定义过滤器,主要负责从请求头中读取token,解析token
5 实现Login controller
6 配置Security,定义那些请求需要进行jwt验证
具体实现
1 配置Maven,增加jwt包依赖
<!--jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2 JWT相关工具类,JWT编解码
package com.springboot.action.saas.modules.security.utils;
import com.springboot.action.saas.modules.security.dto.UserDetailsDto;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Clock;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.DefaultClock;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil implements Serializable {
private static final long serialVersionUID = -3301605591108950415L;
//jwt包提供的时间对象
private Clock clock = DefaultClock.INSTANCE; //时间工具实例
//Header,Payload两部分的签名
@Value("${jwt.secret}")
private String secret;
//超期时间
@Value("${jwt.expiration}")
private Long expiration;
//http请求头字段
@Value("${jwt.header}")
private String tokenHeader;
/*
* 解析jwt字符串
* @param token
* @return Claims对象,jwt信息提供给的一个类
* */
private Claims getAllClaimsFromToken(String token) {
//解析jwt到claims对象
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
/*
* 判定是否token超期
* @param token
* @return boolean
* */
private Boolean isTokenExpired(String token) {
//获取token超期时间
final Date expiration = getExpirationDateFromToken(token);
//判定expiration对象表示的瞬间比clock.now()表示的瞬间早,返回为true
return expiration.before(clock.now());
}
/*
* 获取token外带数据字段,这里是用户名称
* @param token
* @return 用户名称
* */
public String getUsernameFromToken(String token) {
//获取jwt对象
final Claims claims = getAllClaimsFromToken(token);
return claims.getSubject();
}
/*
* 获取token签发时间字段
* @param token
* @return 用户名称
* */
public Date getIssuedAtDateFromToken(String token) {
//获取jwt对象
final Claims claims = getAllClaimsFromToken(token);
return claims.getIssuedAt();
}
/*
* 获取token超期时间字段
* @param token
* @return 超期时间Date对象
* */
public Date getExpirationDateFromToken(String token) {
//获取jwt对象
final Claims claims = getAllClaimsFromToken(token);
return claims.getExpiration();
}
/*
* 判定是否签发时间,是否在修改密码之前
* @param created 签发时间
* @param lastPasswordReset 最后一次密码修改时间
* @return boolean
* */
private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
return (lastPasswordReset != null && created.before(lastPasswordReset));
}
/*
* 生成超期日期对象
* @param createdDate 发放token日期对象
* @return Date 超期日期对象
* */
private Date calculateExpirationDate(Date createdDate) {
//时间戳初始化Date对象
return new Date(createdDate.getTime() + expiration);
}
/*
* 判定是否token超期
* @param userDetails
* @return jwt字符串
* */
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
final Date createdDate = clock.now();
final Date expirationDate = calculateExpirationDate(createdDate);
//创建jwt
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/*
* 刷新token
* @param token token字符串
* @return jwt字符串
* */
public String refreshToken(String token) {
final Date createdDate = clock.now();
final Date expirationDate = calculateExpirationDate(createdDate);
final Claims claims = getAllClaimsFromToken(token);
claims.setIssuedAt(createdDate);
claims.setExpiration(expirationDate);
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/*
* 判定token是否合法
* @param token token字符串
* @param userDetails 验证用户数据
* @return Date 超期日期对象
* */
public Boolean validateToken(String token, UserDetails userDetails) {
//获取认证的用户信息
UserDetailsDto user = (UserDetailsDto)userDetails;
//获取token中的用户名称
final String username = getUsernameFromToken(token);
//获取token中的签发时间
final Date created = getIssuedAtDateFromToken(token);
//获取用户重置密码时间
Date lastPasswordReset = new Date(user.getPasswordResetDate());
//判定token:用户是否合法,是否超期,判定是否
return (
username.equals(user.getUsername())
&& !isTokenExpired(token)
&& !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
);
}
}
3 定义Domain和Dto的数据结构
3.1 用户登陆发送用户信息数据(LoginPwdDto.java)
package com.springboot.action.saas.modules.security.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
//用户帐号密码登陆,用户和密码
@Data
//lombok 注解,NoArgsConstructor 走动生成无参数构造函数
@NoArgsConstructor
public class LoginPwdDto implements Serializable {
//用户帐号
private String username;
//用户密码
private String password;
//对象字符串输出
@Override
public String toString() {
return "{username="
+ username
+ ", password=******}";
}
}
3.2 用户登陆授权通过后给用户返信息数据(LoginInfoDto.java)
package com.springboot.action.saas.modules.security.dto;
import lombok.Data;
import java.io.Serializable;
//用户认证成功后,返回用户信息和jwt的token
@Data
public class LoginInfoDto implements Serializable {
private String token;
private UserDetailsDto user;
public LoginInfoDto(String token, UserDetailsDto user) {
this.token = token;
this.user = user;
}
}
3.3 fliter和认证service之间传递信息数据 (UserDetailsDto.java)
package com.springboot.action.saas.modules.security.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
//这个类,一定比userDto要数据更全面,因为UserDetails在认证后UserDetailsServiceImpl的
//loadUserByUsername函数获取的信息
@Getter
//生成一个全参数的构造函数
@AllArgsConstructor
public class UserDetailsDto implements UserDetails {
//用户idjson返回时不显示)
//在json序列化时将java bean中的一些属性忽略掉
@JsonIgnore
private final Long id;
//用户名称(接口规范必须实现)
private final String username;
//用户密码(json返回时不显示,接口规范必须实现)
@JsonIgnore
private final String password;
//是否禁用
private final Boolean enabled;
@Override
public boolean isEnabled() {
return enabled;
}
//创建时间
private final Long createTime;
//密码重置时间(json返回时不显示)
@JsonIgnore
private final Long passwordResetDate;
//用户权限列表(接口规范必须实现)
@JsonIgnore
private final Collection<GrantedAuthority> authorities;
//(接口规范必须实现)
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
//(接口规范必须实现)
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
//(接口规范必须实现)
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
}
4 实现自定义过滤器,主要负责从请求头中读取token,解析token
package com.springboot.action.saas.modules.security.filter;
import com.springboot.action.saas.modules.security.dto.UserDetailsDto;
import com.springboot.action.saas.modules.security.utils.JwtUtil;
import io.jsonwebtoken.ExpiredJwtException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/*
* 自定义过滤器,主要负责从请求头中读取token,解析token
* 检查token是否合法
* */
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {
//认证用户信息业务对象
private final UserDetailsService userDetailsService;
//jwt 工具对象
private final JwtUtil jwtUtil;
//获取http请求头的key
private final String tokenHeader;
/*
* 构造函数
* @param @Qualifier用来标记service有两个实现类的时候,用那个
* @param jwtUtil jwt工具对象
* @param tokenHeader http请求头token字段
* */
public JwtAuthorizationFilter(@Qualifier("userDetailsServiceImpl") UserDetailsService userDetailsService,
JwtUtil jwtUtil,
@Value("${jwt.header}") String tokenHeader) {
this.userDetailsService = userDetailsService;
this.jwtUtil = jwtUtil;
this.tokenHeader = tokenHeader;
}
/*
* 抽象类oncePerRequestFilter继承自GenericFilterBean,它保留了GenericFilterBean中的所有方法并对之进行了扩展,
* 在oncePerRequestFilter中的主要方法是doFilter。在doFilter方法中,doFilterInternal方法由子类实现,
* 主要作用是规定过滤的具体方法。
* */
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
//获取请求头中key对应的字段
final String requestHeader = request.getHeader(this.tokenHeader);
String username = null;
String token = null;
//处理请求头中的token
if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
//获取token
token = requestHeader.substring(7);
//获取用户名(外带数据)
try {
username = jwtUtil.getUsernameFromToken(token);
} catch (ExpiredJwtException e) {
//发生异常,需要记录日志
//log.error(e.getMessage());
}
}
//用户名判定
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
//获取用户信息
UserDetailsDto userDetailsDto = (UserDetailsDto)this.userDetailsService.loadUserByUsername(username);
//判定token是否合法,不合法走异常处理exceptionHandling().authenticationEntryPoint
if (jwtUtil.validateToken(token, userDetailsDto)) {
//合法,创建带用户名和密码以及权限的Authentication,这里实例化UsernamePasswordAuthenticationToken
//构造函数内实际上已经设置为认证通过super.setAuthenticated(true);
//构造函数3个参数:
// 用户信息(身份认证信息,还有其他外带信息都可以增加)
// 用户密码(于证明principal是正确的信息,比如密码)
// 用户权限(授权信息,比如角色)
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetailsDto,
null,
userDetailsDto.getAuthorities());
//设置获取request的一些http信息,把http的信息放到authentication
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
//记录日志
//log.info("authorizated user '{}', setting security context", username);
//从SecurityContextHolder获取SecurityContext实例,设置authentication
//已验证的主体,或删除身份验证信息
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
//调用后续filter
chain.doFilter(request, response);
}
}
5 实现Login controller,用户验证帐号密码通过后,返回jwt的信息
package com.springboot.action.saas.modules.security.controller;
import com.springboot.action.saas.common.logging.annotation.Log;
import com.springboot.action.saas.common.utils.EncryptionUtils;
import com.springboot.action.saas.modules.security.dto.LoginInfoDto;
import com.springboot.action.saas.modules.security.dto.LoginPwdDto;
import com.springboot.action.saas.modules.security.dto.UserDetailsDto;
import com.springboot.action.saas.modules.security.utils.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.persistence.EntityNotFoundException;
import java.util.List;
/*
* restful 风格接口
* */
//@RestController 代替 @Controller,省略以后的 @ResponseBody
@RestController
//处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。
@RequestMapping("/security")
public class SecurityController {
//jwt工具对象,根据类型来查找和自动装配元素的
@Autowired
private JwtUtil jwtUtil;
//认证用户信息对象
@Autowired
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
/**
* 用户帐号,密码登陆
* @param loginPwdDto 用户的帐号和密码
* @return LoginInfoDto
*/
@Log("帐号登陆")
@PostMapping(value = "/pwdlogin")
public LoginInfoDto pwdlgoin(@Validated @RequestBody LoginPwdDto loginPwdDto) {
try {
//获取用户认证信息
final UserDetailsDto userDetailsDto = (UserDetailsDto)userDetailsService.loadUserByUsername(loginPwdDto.getUsername());
//判定密码是否正确
final String userPassword = EncryptionUtils.encryptPassword(loginPwdDto.getPassword());
if (!userDetailsDto.getPassword().equals(userPassword)) {
//密码错误处理,抛异常
throw new AccountExpiredException("密码错误");
}
//判定用户是否启动
if (!userDetailsDto.isEnabled()) {
//处理帐号禁用,抛异常
throw new AccountExpiredException("帐号被禁用");
}
//生成token
String token = jwtUtil.generateToken(userDetailsDto);
//返回认证信息对象
return new LoginInfoDto(token, userDetailsDto);
} catch (EntityNotFoundException e) {
//检查用户名是否存在
throw new AccountExpiredException("用户不存在");
}
}
}
6 配置Security,用jwt的认证接管默认的帐号密码认证
package com.springboot.action.saas.modules.security.config;
import com.springboot.action.saas.modules.security.filter.JwtAuthorizationFilter;
import com.springboot.action.saas.modules.security.service.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
//定义配置类被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被
//AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,
//并用于构建bean定义,初始化Spring容器。
@Configuration
//加载了WebSecurityConfiguration配置类, 配置安全认证策略。
//加载了AuthenticationConfiguration,
@EnableWebSecurity
//用来构建一个全局的AuthenticationManagerBuilder的标志注解
//开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认
@EnableGlobalMethodSecurity(prePostEnabled = true)
//Web Security 配置类
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//实现UserDetailService接口用来做登录认证
@Autowired
private UserDetailsServiceImpl userDetailsServiceImpl;
//自定义基于JWT的安全过滤器,bean
@Autowired
JwtAuthorizationFilter authenticationFilter;
/*
* 配置http服务,路径拦截、csrf保护等等均可通过此方法配置
* */
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
//HttpSecurity对象
httpSecurity
// 禁用 CSRF,不然post调试的时候都403
.csrf().disable()
// 由于使用jwt,不创建会话
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 设置权限定义哪些URL需要被保护、哪些不需要被保护。HttpSecurity对象的方法
.authorizeRequests()
// 过滤请求,允许对网站静态资源的访问,无需授权
.antMatchers(
HttpMethod.GET,
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js"
).permitAll()
// 登陆页面,无需授权
.antMatchers(HttpMethod.POST, "/security/pwdlogin").permitAll()
// 调试期间先允许访问
//.antMatchers("/member/**").permitAll()
// 认证通过后任何请求都可访问。AbstractRequestMatcherRegistry的方法
.anyRequest().authenticated()
// 连接HttpSecurity其他配置方法
.and()
// 生成默认登录页,HttpSecurity对象的方法
.formLogin();
// 增加jwt filter
httpSecurity
.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
/**
* 设定PsswordEncoder为BeanBcrypt加密方式,后面在设定AuthenticationProvider需要用到
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoderBean() {
return new BCryptPasswordEncoder();
}
/**
* 创建认证提供者Bean
* DaoAuthenticationProvider是SpringSecurity提供的AuthenticationProvider默认实现类
* 授权方式提供者,判断授权有效性,用户有效性,在判断用户是否有效性,
* 它依赖于UserDetailsService实例,可以自定义UserDetailsService的实现。
*
* @return
*/
@Bean
public DaoAuthenticationProvider authenticationProvider() {
// 创建DaoAuthenticationProvider实例
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
// 将自定义的认证逻辑添加到DaoAuthenticationProvider
authProvider.setUserDetailsService(userDetailsServiceImpl);
// 设置自定义的密码加密
authProvider.setPasswordEncoder(passwordEncoderBean());
return authProvider;
}
/*
* 配置好的认证提供者列表
*
* */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 添加自定义的认证逻辑
auth.authenticationProvider(authenticationProvider());
}
}
网友评论