1 引文
后台开发,必然涉及到接口开发,接口即有安全性的问题,综合解决这个问题,涉及到多个方面, 如不注意,可能只关注到其中的个别基本点。
关于安全问题有哪些方面,这篇文章《Web登录其实没你想的那么简单》有详细介绍
即便基于相同的目标点,解决的方案也有很多。这篇文章就来探究一下Token这个方案在restul的实践。
2 JWT介绍
按照wiki上的定义,它是一个开放的基于JSON的安全标准,主要用于授权和信息签名交换。
它一般由三段组成:header/payload/signature,再由.拼接。比如下面这段:
-
第一部分,token的类型及算法,然后用base64编码
-
第二部分,payload是数据部分,然后再用base64编码。与第一部分一样,都不要放置敏感信息
-
第三部分,签名部分,
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
,用于验证token的签发安全。
更多介绍可以查看官网
3 JWT示例
3.1 JWT示例1
参考自这里,也实践应用过,用户信息就是idUser,登录后产生一个会话idSession,每次登录会变。 代入为password,相关细节相对于github上的代码有一些变更,实际情况见下方的代码。
定义一个redis管理器:
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.beans.factory.annotation.Autowired;
public class ShiroRedisCacheManager implements CacheManager {
@Autowired
private ShiroRedisCache shiroRedisCache;
@Override
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
return shiroRedisCache;
}
}
关于的redis操作类:
import com.newread.ggh.utils.Utils;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.SerializationUtils;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public class ShiroRedisCache<K,V> implements Cache<K,V> {
//@Autowired默认按照类型<String, byte[]>装配的,不成功!@Resource默认按照名字装配
//https://blog.csdn.net/zhaoheng314/article/details/81564166
@Resource
private RedisTemplate<byte[], byte[]> redisTemplate;
// @Resource(name="redisTemplate")
// ValueOperations<byte[], byte[]> valOpsObj;
private final String CACHE_PREFIX = "shiro-redis-cache";
private byte[] getKey(K k){
if(k instanceof String){
return (CACHE_PREFIX + k).getBytes();
}
return SerializationUtils.serialize(k);
}
// private String getKey(K k){
// if (k instanceof String) {
// return CACHE_PREFIX + k;
// }
// return CACHE_PREFIX + new String(SerializationUtils.serialize(k));
// }
@Override
public V get(K k) throws CacheException {
String key = new String(getKey(k));
if(!Utils.countingBloomFilter.contains(key)){
return null;
}
byte[] value = redisTemplate.opsForValue().get(key);
if(value != null){
return (V)SerializationUtils.deserialize(value);
}
return null;
}
@Override
public V put(K k, V v) throws CacheException {
byte[] key = getKey(k);
byte[] value = SerializationUtils.serialize(v);
redisTemplate.opsForValue().set(key, value, 24, TimeUnit.HOURS);
return v;
}
@Override
public V remove(K k) throws CacheException {
if (k == null) return null;
byte[] key = getKey(k);
byte[] value = redisTemplate.opsForValue().get(key);
redisTemplate.delete(key);
if(value != null ){
return (V)SerializationUtils.deserialize(value);
}
return null;
}
@Override
public void clear() throws CacheException {
}
@Override
public int size() {
return 0;
}
@Override
public Set<K> keys() {
return null;
}
@Override
public Collection<V> values() {
return null;
}
}
鉴权策略:
import com.mysql.jdbc.StringUtils;
import com.newread.ggh.bean.User;
import com.newread.ggh.service.CacheService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class MyRealm extends AuthorizingRealm {
//https://blog.csdn.net/elonpage/article/details/78965176 实例化顺序的原因?此处需要@Resource + @Lazy
@Resource
@Lazy
private CacheService cacheService;
/**
* 大坑!,必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
/**
* 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//org.apache.shiro.realm.AuthorizingRealm.getAuthorizationInfo先判断有无,无则进入此方法
//if (info == null) {
// info = this.doGetAuthorizationInfo(principals);
//TODO 更新用户角色时需要更新cache
long idUser = JWTUtil.getUserId(principals.toString());
if (idUser == 0) {
throw new AuthenticationException("token错误");
}
User user = cacheService.findUserById(idUser);
if (user == null) {
throw new AuthenticationException("token用户不存在");
}
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRoles(StringUtils.split(user.getRoleNames(), ",", true));
//权限暂未细分
//Set<String> permission = new HashSet<>(Arrays.asList(user.getPermission().split(",")));
//simpleAuthorizationInfo.addStringPermissions(permission);
return simpleAuthorizationInfo;
}
/**
* 默认使用此方法进行用户编号和会话编号正确与否验证,错误抛出异常即可。
*
* TODO AuthenticationException 在rest接口统一异常处理无法截获
* https://my.oschina.net/liululee/blog/1808027
*
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();
// 解密获得username,用于和数据库进行对比
long idUser = JWTUtil.getUserId(token);
if (idUser == 0) {
// https://blog.csdn.net/qq_28637575/article/details/78590319
throw new IncorrectCredentialsException("token错误");
}
User user = cacheService.findUserById(idUser);
if (user == null) {
throw new IncorrectCredentialsException("token用户不存在");
}
if (!JWTUtil.verify(token, idUser, user.getIdSession())) {
throw new ExpiredCredentialsException("token验证未通过,或已失效?");
resolver.handlerException(request, response, null, new WelendException(StatusCode.CAPTCHA_ERROR));
}
return new SimpleAuthenticationInfo(token, token, "my_realm");
}
}
工具类:
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.apache.shiro.authz.UnauthorizedException;
import java.io.UnsupportedEncodingException;
public class JWTUtil {
// 过期时间5分钟
//TODO 跟缓存session id的时间同步
//private static final long EXPIRE_TIME = 5*60*1000;
/**
* 校验token是否正确
* @param token 密钥
* @param secret 会话Session id
* @return 是否正确
*/
public static boolean verify(String token, long idUser, long idSession) {
try {
Algorithm algorithm = Algorithm.HMAC256(String.valueOf(idSession));
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("idUser", idUser)
.build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
/**
* 获得token中的信息无需secret解密也能获得
* @return token中包含的用户编号
*/
public static long getUserId(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("idUser").asLong();
} catch (JWTDecodeException e) {
throw new UnauthorizedException("用户会话不存在");
}
}
/**
* 生成签名,5min后过期
* @param idUser 用户编号
* @param idSession 用户会话编号,代替传统的密码
* @return 加密的token
*/
public static String sign(long idUser, long idSession) {
try {
//Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(String.valueOf(idSession));
// 附带username信息
return JWT.create()
.withClaim("idUser", idUser)
//.withExpiresAt(date)
.sign(algorithm);
} catch (UnsupportedEncodingException e) {
return null;
}
}
}
import org.apache.shiro.authc.AuthenticationToken;
public class JWTToken implements AuthenticationToken {
// 密钥
private String token;
public JWTToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
filter定义:
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JWTFilter extends BasicHttpAuthenticationFilter {
private Logger LOGGER = LoggerFactory.getLogger(this.getClass());
/**
* 判断用户是否想要登入。
* 检测header里面是否包含Authorization字段即可
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
//HttpServletRequest req = (HttpServletRequest) request;
//String authorization = req.getHeader("Authorization");
return getAuthzHeader(request) != null;
}
/**
*
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
//HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//httpServletRequest.getHeader("Authorization");
String authorization = getAuthzHeader(request);//获得header为Authorization的值
JWTToken token = new JWTToken(authorization);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(token);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 这里我们详细说明下为什么最终返回的都是true,即允许访问
* 例如我们提供一个地址 GET /article
* 登入用户和游客看到的内容是不同的
* 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
* 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
* 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
* 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginAttempt(request, response)) {
//try {
executeLogin(request, response);
//} catch (Exception e) {
//response401(request, response);
//}
}
return true;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* 将非法请求跳转到 /401
*/
private void response401(ServletRequest req, ServletResponse resp) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
httpServletResponse.sendRedirect("/401");
} catch (IOException e) {
LOGGER.error(e.getMessage());
}
}
}
将以上注入到容器:
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean("securityManager")
public DefaultWebSecurityManager getManager(MyRealm realm, ShiroRedisCacheManager shiroRedisCacheManager) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 使用自己的realm
manager.setRealm(realm);
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
//https://www.jianshu.com/p/9701d3d44524
manager.setCacheManager(shiroRedisCacheManager);
return manager;
}
@Bean("shiroRedisCacheManager")
public ShiroRedisCacheManager shiroRedisCacheManager(){
ShiroRedisCacheManager shiroRedisCacheManager = new ShiroRedisCacheManager();
return shiroRedisCacheManager;
}
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("jwt", new JWTFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
factoryBean.setUnauthorizedUrl("/401");
/*
* 自定义url规则
* http://shiro.apache.org/web.html#urls-
*/
Map<String, String> filterRuleMap = new HashMap<>();
// 所有请求通过我们自己的JWT Filter
filterRuleMap.put("/**", "jwt");
// 访问401和404页面不通过我们的Filter
filterRuleMap.put("/401", "anon");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
/**
* 下面的代码是添加注解支持
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
// https://zhuanlan.zhihu.com/p/29161098
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
登录之后写token:
String token = JWTUtil.sign(u.getId(), u.getIdSession());
如果写在缓存中,注销登录只要清除缓存:
utils.shiroRedisUtils().remove(u.getToken());
应用的方法也是非常简单,直接在controller接口上注解,比如:
@RequiresAuthentication
@RequiresRoles(logical = Logical.OR, value = {"book_mgr"})
这个方案的几个特点是:
- 可以灵活开放接口,只需要加个注解,除此,与接口没有任何耦合,比如下列代码是另一个方案的,则有不同:
@RestController
@RequestMapping("/token")
public class AuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserService userService;
@RequestMapping(value = "/generate-token", method = RequestMethod.POST)
public ApiResponse<AuthToken> register(@RequestBody LoginUser loginUser) throws AuthenticationException {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword()));
final User user = userService.findOne(loginUser.getUsername());
final String token = jwtTokenUtil.generateToken(user);
return new ApiResponse<>(200, "success",new AuthToken(token, user.getUsername()));
}
}
- 跟缓存无缝整合,不足:如果用户角色更新后,cache未能很好更新,暂行解决办法——清理缓存,重新登录。TODO 缓存失效
3.2 JWT示例2
这个示例的代码在这里,相关博文在这里, 这个示例看起来也有学习价值,虽然关注度非常低。
3.2.1 读代码
初看了一下,感觉其特点:
- 与spring boot security整合在一起,exception的entry point似乎可以解决filter的鉴权异常采用自定义格式化输出的问题
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().
authorizeRequests()
.antMatchers("/token/*", "/signup").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
}
- 其在user service处初始化权限数据
@Autowired
private UserDao userDao;
@Autowired
private BCryptPasswordEncoder bcryptEncoder;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.findByUsername(username);
if(user == null){
throw new UsernameNotFoundException("Invalid username or password.");
}
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), getAuthority());
}
private List<SimpleGrantedAuthority> getAuthority() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
- 应用有点耦合,初始化的方法当然没有问题,毕竟需要获得用户的角色等数据
@Service(value = "userService")
public class UserServiceImpl implements UserDetailsService, UserService {
@Autowired
private UserDao userDao;
@Autowired
private BCryptPasswordEncoder bcryptEncoder;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.findByUsername(username);
if(user == null){
throw new UsernameNotFoundException("Invalid username or password.");
}
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), getAuthority());
}
private List<SimpleGrantedAuthority> getAuthority() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
登录获得token:
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/token")
public class AuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserService userService;
@RequestMapping(value = "/generate-token", method = RequestMethod.POST)
public ApiResponse<AuthToken> register(@RequestBody LoginUser loginUser) throws AuthenticationException {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword()));
final User user = userService.findOne(loginUser.getUsername());
final String token = jwtTokenUtil.generateToken(user);
return new ApiResponse<>(200, "success",new AuthToken(token, user.getUsername()));
}
}
引用的generateToken详情如下,这里角色都是写死了,当然可以进一步处理:
public String generateToken(User user) {
return doGenerateToken(user.getUsername());
}
private String doGenerateToken(String subject) {
Claims claims = Jwts.claims().setSubject(subject);
claims.put("scopes", Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
return Jwts.builder()
.setClaims(claims)
.setIssuer("http://devglan.com")
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY_SECONDS*1000))
.signWith(SignatureAlgorithm.HS256, SIGNING_KEY)
.compact();
}
在filter里这样验证:
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
logger.info("authenticated user " + username + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
3.2.2 代码改良
代码包下载地址,有一些小的修改:
-
修改application.properties,配置mysql的连接参数
-
修改
vim src/main/java/com/devglan/config/WebSecurityConfig.java
,放到对/users的权限 -
将pom.xml改动一下
<packaging>jar</packaging>
<properties>
<failOnMissingWebXml>false</failOnMissingWebXml>
<start-class>com.devglan.Application</start-class>
<java.version>1.8</java.version>
</properties>
-
打包,运行命令:
"D:\dev\ideaIU-2018.2.5\plugins\maven\lib\maven3\bin\mvn" clean compile install
-
启动,
java -jar target/spring-boot-jwt-1.0-SNAPSHOT.jar
-
新增用户,POST接口
http://localhost:8080/users
新增用户,传参数:{"username":"tao","password":"2","id":2}
-
修改application.properties,改为update,即
spring.jpa.hibernate.ddl-auto=update
-
修改
vim src/main/java/com/devglan/config/WebSecurityConfig.java
,改回,限制/users的权限 -
调POST接口
http://localhost:8080/token/generate-token
,传参{"username":"tao","password":"1"}
,获取token,返回:
{
"status": 200,
"message": "success",
"result": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0YW8iLCJzY29wZXMiOlt7ImF1dGhvcml0eSI6IlJPTEVfQURNSU4ifV0sImlzcyI6Imh0dHA6Ly9kZXZnbGFuLmNvbSIsImlhdCI6MTU1NDE3MDI2NCwiZXhwIjoxNTU0MTg4MjY0fQ.d3XcnHgPld5r5jt3WiKJNAFjGs7_ejkuSG-Y9ykkTqs",
"username": "tao"
}
}
- 调GET接口
http://localhost:8080/users/1
,传query参数Authorization
值为Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0YW8iLCJzY29wZXMiOlt7ImF1dGhvcml0eSI6IlJPTEVfQURNSU4ifV0sImlzcyI6Imh0dHA6Ly9kZXZnbGFuLmNvbSIsImlhdCI6MTU1NDE3MDI2NCwiZXhwIjoxNTU0MTg4MjY0fQ.d3XcnHgPld5r5jt3WiKJNAFjGs7_ejkuSG-Y9ykkTqs
, 注意前面是加了Bearer
的。返回正常:
{
"status": 200,
"message": "User fetched successfully.",
"result": {
"id": 1,
"firstName": null,
"lastName": null,
"username": "tao",
"salary": 0,
"age": 0
}
}
如果错误token,返回:
{
"timestamp": "2019-04-02T02:00:40.238+0000",
"status": 401,
"error": "Unauthorized",
"message": "Unauthorized1111111111111111111111111111111111111111111111111111111111111111",
"path": "/users/1"
}
那么多1是在entry point里处理异常时特别加的response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized1111111111111111111111111111");
,为了验证这个point有效。不过这个json仅定制了它的message,整个json的结构并没有变化,比如我想返回这种:
{
"status": "INTERNAL_SERVER_ERROR",
"timestamp": "2019-04-03 00:40:13",
"errcode": 1111,
"errmsg": "查不到该用户的图书馆信息"
}
3.2.3 效果
每次传错误的token都返回的401错误,这是期望的效果,如何达到的,关键代码在哪呢?应该是下面两部分:
jwtTokenUtil.validateToken(authToken, userDetails)
以及在spring security里指定的exception handler:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().
authorizeRequests()
.antMatchers("/token/*", "/signup").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
}
更多代码,可以直接看这里