1 引文

后台开发,必然涉及到接口开发,接口即有安全性的问题,综合解决这个问题,涉及到多个方面, 如不注意,可能只关注到其中的个别基本点。

关于安全问题有哪些方面,这篇文章《Web登录其实没你想的那么简单》有详细介绍

即便基于相同的目标点,解决的方案也有很多。这篇文章就来探究一下Token这个方案在restul的实践。

2 JWT介绍

按照wiki上的定义,它是一个开放的基于JSON的安全标准,主要用于授权和信息签名交换。

它一般由三段组成:header/payload/signature,再由.拼接。比如下面这段:

2019-04-01_20-22-42.jpg

  • 第一部分,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);
}

更多代码,可以直接看这里