JWT 和 Spring Security 保护 REST API 第二篇
接着《JWT 和 Spring Security 保护 REST API第一篇》继续来配置,我们已经把SpringSecurity部分配置完了,现在,我们要开始配置JWT 了
首先,我们我们来看下目录,大致了解下我们需要的类
首先,我们要在配置文件application.yml里面进行jwt的相关配置
每次客户端向服务端发送请求的时候head里面都要带上Authorization的属性,route是关于加密请求与生成令牌路径的相关配置:/auth是获取令牌路径;/refresh是刷新令牌路径(当我们后面要拿这个路径的时候,要从这个配置文件里面来读取)
接着,我们要创建一个过滤器,所以首先先要创建一些过滤器中需要用到的类
我们先创建JwtAuthenticationRequest,它是与jwt校验相关的请求类,它也可以当做一个实体来使用,里面只有用户名和密码属性,这是我们做安全服务时,校验用户名和密码的时候使用的类(从请求里面获取用户名和密码,把它封装成这个类)
package com.yy.hospital.security.domain;
import java.io.Serializable;
/**
* 在请求中获得用户名和密码然后封装起来
*/
public class JwtAuthenticationRequest implements Serializable {
private static final long serialVersionUID = -8445943548965154778L;
private String username;
private String password;
public JwtAuthenticationRequest() {
super();
}
public JwtAuthenticationRequest(String username, String password) {
this.setUsername(username);
this.setPassword(password);
}
//这里,getter和setter省略
}
接着在写JwtAuthenticationResponse,这个类里面只有一个不可更改的属性----token,也就是令牌。通过安全验证得到的结果响应封装在这个类里面。
package com.yy.hospital.security.domain;
import java.io.Serializable;
/**
* 响应令牌类
*/
public class JwtAuthenticationResponse implements Serializable {
private static final long serialVersionUID = 4784951536404964122L;
private final String token; //要发送回客户端的令牌
public JwtAuthenticationResponse(String token) {
this.token = token;
}
public String getToken() {
return this.token;
}
}
请求和响应需要用到的封装对象就完成了,接下来我们再写个异常类AuthenticationException
package com.yy.hospital.security.domain;
/**
* 授权异常类
*/
public class AuthenticationException extends RuntimeException {
public AuthenticationException(String message, Throwable cause) {
super(message, cause);
}
}
最后我们再写一个JwtAuthenticationEntryPoint类,。它实现了AuthenticationEntryPoint接口,然后在类里面重写了commence方法。当抛出授权抛异常后,就向客户端响应未授权的异常信息。
package com.yy.hospital.security.domain;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;
/**
* 抛异常后,向客户端响应的信息类(未授权的响应信息)
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
// This is invoked when user tries to access a secured REST resource without supplying any credentials
// We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
上面这四个类都放在domain包里,因为它们都是一些准备好被别的类调用的承载信息的类(封装了相关数据的类,需要在后面去使用)
现在我们要写几个重要的类了,首先是生成令牌的工具JwtTokenUtil。它里面提供了生成令牌和从客户端请求中获取的令牌里面拿相关字段两个功能(涉及解密的过程),即解析令牌信息与生成令牌信息,所以非常重要。
package com.yy.hospital.security;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import com.yy.hospital.security.domain.JwtUser;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
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.mobile.device.Device;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
/**
* jwt工具类,生成令牌
* 从客户端获取的令牌里面获取相关字段(解密)
*/
@Component
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = -3301605591108950415L;
//申明部分的属性名
static final String CLAIM_KEY_USERNAME = "sub";
static final String CLAIM_KEY_AUDIENCE = "aud";
static final String CLAIM_KEY_CREATED = "iat";
//签名部分的属性名
static final String AUDIENCE_UNKNOWN = "unknown";
static final String AUDIENCE_WEB = "web";
static final String AUDIENCE_MOBILE = "mobile";
static final String AUDIENCE_TABLET = "tablet";
@SuppressFBWarnings(value = "SE_BAD_FIELD", justification = "It's okay here")
private Clock clock = DefaultClock.INSTANCE;
@Value("${jwt.secret}") //从配置文件去获取自定义口令,然后注入到secret中来
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
//从得到的令牌里面获得用户名
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public Date getIssuedAtDateFromToken(String token) {
return getClaimFromToken(token, Claims::getIssuedAt);
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public String getAudienceFromToken(String token) {
return getClaimFromToken(token, Claims::getAudience);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(clock.now());
}
private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
return (lastPasswordReset != null && created.before(lastPasswordReset));
}
private String generateAudience(Device device) {
String audience = AUDIENCE_UNKNOWN;
if (device.isNormal()) {
audience = AUDIENCE_WEB;
} else if (device.isTablet()) {
audience = AUDIENCE_TABLET;
} else if (device.isMobile()) {
audience = AUDIENCE_MOBILE;
}
return audience;
}
private Boolean ignoreTokenExpiration(String token) {
String audience = getAudienceFromToken(token);
return (AUDIENCE_TABLET.equals(audience) || AUDIENCE_MOBILE.equals(audience));
}
//按照传入的用户userDetails和Token的规则生成令牌
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}
//生成令牌
private String doGenerateToken(Map<String, Object> claims, String subject) {
final Date createdDate = clock.now();
final Date expirationDate = calculateExpirationDate(createdDate);
System.out.println("doGenerateToken " + createdDate);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
final Date created = getIssuedAtDateFromToken(token);
return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
&& (!isTokenExpired(token) || ignoreTokenExpiration(token));
}
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();
}
public Boolean validateToken(String token, UserDetails userDetails) {
JwtUser user = (JwtUser) userDetails;
final String username = getUsernameFromToken(token);
final Date created = getIssuedAtDateFromToken(token);
//final Date expiration = getExpirationDateFromToken(token);
return (
username.equals(user.getUsername())
&& !isTokenExpired(token)
&& !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate())
);
}
private Date calculateExpirationDate(Date createdDate) {
return new Date(createdDate.getTime() + expiration * 1000);
}
}
我自己这个项目主要用到里面的getUsernameFromToken(再次登录拿值)和doGenerateToken(初次登录生成令牌),还有validateToken(校验token)三个方法
然后,我们就可以配置一个过滤器,每次请求要把这个令牌从这个请求的信息里面过滤出来
package com.yy.hospital.security;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import io.jsonwebtoken.ExpiredJwtException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
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.UserDetails;
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;
/**
* JWT 过滤器;校验令牌信息(配在请求访问里面的)
* 这个类还要配置到WebSecurityConfig
*/
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private final Log logger = LogFactory.getLog(this.getClass());
private UserDetailsService userDetailsService;
private JwtTokenUtil jwtTokenUtil;
private String tokenHeader; //客户端发过来的请求(Authorization)属性值
public JwtAuthenticationTokenFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, String tokenHeader) {
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
this.tokenHeader = tokenHeader;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
final String requestHeader = request.getHeader(this.tokenHeader);
String username = null;
String authToken = null;
if (requestHeader != null && requestHeader.startsWith("Bearer ")) { //Bearer 是承载字符串
//从客户端请求里面获取
authToken = requestHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(authToken);
} catch (IllegalArgumentException e) {
logger.error("an error occured during getting username from token", e);
} catch (ExpiredJwtException e) {
logger.warn("the token is expired and not valid anymore", e);
}
} else {
logger.warn("couldn't find bearer string, will ignore the header");
}
logger.info("checking authentication for user " + username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// It is not compelling necessary to load the use details from the database. You could also store the information
// in the token and read it from it. It's up to you ;)
// 获取了令牌的用户信息
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// For simple validation it is completely sufficient to just check the token integrity. You don't have to call
// the database compellingly. Again it's up to you ;)
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
logger.info("authenticated user " + username + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
}
过滤器里面主要的方法是doFilterInternal。
1)它先从请求里面获得头文件数据(request.getHeader(this.tokenHeader))requestHeader里面包含了令牌的信息
2)在判断requestHeader是否为空,不为空时是否有固定的承载字符串 “Bearer ”,如果不为空且有承载字符串,就去掉承载字符串,得到authToken,即令牌
3)然后再调用jwtTokenUtil里面getUsernameFromToken(authToken)方法取出用户名。
4)取到用户名username后,我们再调用userDetailsService中的loadUserByUsername(username)方法,得到jwtUser对象
5)然后我们再把令牌和查到的用户对象放作为参数放到jwtTokenUtil中的validateToken(authToken,userDetails)方法中来校验
6)如果校验成功,就把用户授权的角色集合取出来,然后我们访问后端的api就可以按角色来访问了
所以我们可以看出,过滤器类主要作用就是校验令牌的。所以这个校验肯定是要配在请求访问里面的,即web访问里,所以我们现在要把过滤器配到WebSecurityConfig里面去
package com.yy.hospital.security.config;
import com.yy.hospital.security.JwtAuthenticationTokenFilter;
import com.yy.hospital.security.JwtTokenUtil;
import com.yy.hospital.security.domain.JwtAuthenticationEntryPoint;
import com.yy.hospital.security.service.JwtUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.builders.WebSecurity;
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;
/**
* Web 安全配置类
*
*/
@Configuration //这是配置类
@EnableWebSecurity //允许webSecurity进行检查
@EnableGlobalMethodSecurity(prePostEnabled = true) //对全局的所有方法都要做安全检查类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
//未授权响应处理类
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private JwtUserDetailsService jwtUserDetailsService;
@Value("${jwt.header}")
private String tokenHeader;
@Value("${jwt.route.authentication.path}")
//获取授权令牌的路径(配置文件里面)
private String authenticationPath;
@Autowired
//配置一个全局的授权管理器
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(jwtUserDetailsService)
.passwordEncoder(passwordEncoderBean());
}
@Bean
public PasswordEncoder passwordEncoderBean() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
//授权管理器
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//configure分两部分写
//安全访问的策略
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// we don't need CSRF because our token is invulnerable
.csrf().disable()
.cors().and() // 支持跨域访问
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// don't create session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll() //未授权的情况下都能访问的路径
.anyRequest().authenticated(); //其他的request都需要校验
// Custom JWT based security filter
//创建过滤器,过滤jwt请求
JwtAuthenticationTokenFilter authenticationTokenFilter=new JwtAuthenticationTokenFilter(userDetailsService(), jwtTokenUtil, tokenHeader);
httpSecurity //把过滤器添加到安全策略里面去
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// disable page caching
httpSecurity
.headers()
.frameOptions().sameOrigin() // required to set for H2 else H2 Console will be blank.
.cacheControl();
}
@Override
public void configure(WebSecurity web) throws Exception {
// AuthenticationTokenFilter will ignore the below paths
web
.ignoring()
.antMatchers(
HttpMethod.POST,
authenticationPath //从配置文件里面读取出来的
)
// allow anonymous resource requests
.and()
.ignoring()
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js"
);
}
}
现在,我们来写几个安全访问的controller
package com.yy.hospital.security.controller;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;
import com.yy.hospital.security.JwtTokenUtil;
import com.yy.hospital.security.domain.AuthenticationException;
import com.yy.hospital.security.domain.JwtAuthenticationRequest;
import com.yy.hospital.security.domain.JwtAuthenticationResponse;
import com.yy.hospital.security.domain.JwtUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.*;
/**
* 授权控制器
* 用户登录控制器
*
*/
@RestController
@RequestMapping("/api")
public class AuthenticationRestController {
@Value("${jwt.header}")
private String tokenHeader;
//WebSecurityConfig类里面配置的,用来校验用户名和密码的
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
@Qualifier("jwtUserDetailsService")
private UserDetailsService userDetailsService;
/**
* 创建授权令牌 (登录)
*
* @param authenticationRequest
* @return
* @throws AuthenticationException
*/
@RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST) //用户登录的用户名和密码已经封装到JwtAuthenticationRequest里面了
public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtAuthenticationRequest authenticationRequest) throws AuthenticationException {
//authenticate校验用户名和密码(本类下面)
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
//校验通过后
// Reload password post-security so we can generate the token
//按用户名查用户
final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
//然后传入用户生成令牌token
final String token = jwtTokenUtil.generateToken(userDetails);
//把token封装到JwtAuthenticationResponse里面返回
// Return the token
return ResponseEntity.ok(new JwtAuthenticationResponse(token));
}
/**
* 刷新
*
* @param request
* @return
*/
@RequestMapping(value = "${jwt.route.authentication.refresh}", method = RequestMethod.GET)
public ResponseEntity<?> refreshAndGetAuthenticationToken(HttpServletRequest request) {
String authToken = request.getHeader(tokenHeader);
final String token = authToken.substring(7);
String username = jwtTokenUtil.getUsernameFromToken(token);
JwtUser user = (JwtUser) userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.canTokenBeRefreshed(token, user.getLastPasswordResetDate())) {
String refreshedToken = jwtTokenUtil.refreshToken(token);
return ResponseEntity.ok(new JwtAuthenticationResponse(refreshedToken));
} else {
return ResponseEntity.badRequest().body(null);
}
}
@ExceptionHandler({AuthenticationException.class})
public ResponseEntity<String> handleAuthenticationException(AuthenticationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
}
/**
* Authenticates the user. If something is wrong, an {@link AuthenticationException} will be thrown
*/
private void authenticate(String username, String password) {
Objects.requireNonNull(username);
Objects.requireNonNull(password);
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
} catch (DisabledException e) {
throw new AuthenticationException("User is disabled!", e);
} catch (BadCredentialsException e) {
throw new AuthenticationException("Bad credentials!", e);
}
}
}