spring cloud进阶七 [jwt服务间的鉴权]

一、为什么要使用jwt?


在微服务架构下的服务基本都是无状态的,传统的使用session的方式不再适用,如果使用的话需要做同步session机制,所以产生了了一些技术来对微服务架构进行保护,例如常用的鉴权框架Spring Security OAuth2和用Jwt来进行保护,相对于框架而言,jwt较轻,且可以自包含一些用户信息和设置过期时间,省去了Spring Security OAuth2繁琐的步骤。

二、什么是JWT?


jwt(JSON WEB TOKEN)是一种用来在网络上声明某种身份的令牌(TOKEN),它的特点是紧凑且自包含并且基于JSON,通过一些常用的算法对包含的主体信息进行加密,安全性很高。它通常有三个部分组成:头信息(Header),消息体(Payload),签名(Signature)。
Header通常用来声明令牌的类型和使用的算法,Payload主要用来包含用户的一些信息,Signature部分则是将Base64编码后的Header和Payload进行签名。

三、在Spring Cloud 下如何使用 jwt?


在SC(Spring Cloud简称,以下将都采用这种方式)下通常使用需要安全保护的有两处,分别为系统认证和服务内部鉴权。
1系统认证
(1)基本流程
jwt基本使用方式如下图

用户在提交登录信息后,服务器校验数据后将通过密钥的方式来生成一个字符串token返回给客户端,客户端在之后的请求会把token放在header里,在请求到达服务器后,服务器会检验和解密token,如果token被篡改或者失效将会拒绝请求,如果有效则服务器可以获得用户的相关信息并执行请求内容,最后将结果返回。
在微服务架构下,通常有单独一个服务Auth去管理相关认证,为了安全不会直接让用户访问某个服务,会开放一个入口服务作为网关gateway,只允许外网网关,所有请求首先访问gateway,有gateway将请求路由到各个服务,spring cloud下通常使用zuul来实现网关,整个基本过程如下图所示

客户端请求网关后,网关会根据路径过滤请求,是登录获取token操作的路径则直接放行,请求直接到达auth服务进行登录操作,之后进行JWT私钥加密生成token返回给客户端;是其他请求将会进行token私钥解密校验,如果token被篡改或者失效则直接拒绝访问并返回错误信息,如果验证成功经过路由到达请求服务,请求服务响应并返回数据。
(2)如何实现登录、刷新、注销等?
登录比较简单,在验证身份信息后可以使用工具包例如jjwt根据用户信息生成token并设置有效时长,最后将token返回给客户端存储即可,客户端只需要每次访问时将token加在请求头里即可,然后在zuul增加一个filter,此filter来过滤请求,如果是登录获取token则放行,其他的话用公钥解密验证token是否有效。
如果要实现刷新,则需要在生成token时生成一个refreshKey,在登录时和token一并返回给客户端,然后由客户端保存定时使用refreshKey和token来刷新获取新的有效时长的token,这个refreshKey可自定义生成,为了安全起见,服务器可能需要缓存refreshKey,可使用redis来进行存储,每次刷新token都将生成新的refreshKey和token,服务器需要将老refreshKey替换,客户端保存新的token和refreshKey来进行之后的访问和刷新。
如果要实现注销,并使得旧的token即便在有效期内也不能通过验证,则需要修改登录、刷新、和优化zuul的filter。首先在登录时生成token和refreshKey后,需要将token也进行缓存,如果通过redis进行缓存可以直接放一个Set下,此Set存储所有未过期的token。其次,在刷新时在这个Set中删除旧的token并放入新的。最后对zuulFilter进行优化,在解密时先从redis里存放token的Set查找此token是否存在(redis的Set有提供方法),如果没有则直接拒绝,如果有再进行下一步解密验证有效时长,验证有效时长是为了防止刷新机制失效、没有刷新机制、网络异常强行退出等事件出现,在这种情况下旧的token没有被删除,导致了旧的token一直可以访问(如果只验证是否token是否在缓存中)。在注销时只需要删除redis中Set的token记录就好,最后写个定时器去定时删除redis中Set里面过时的token,原因也是刷新机制失效、没有刷新机制、网络异常强行退出等事件出现导致旧的token没有被删除。

四、JWT存在的问题


jwt第一次生成token 的时候会比较慢,而且因为采用了加密算法保证安全,所以比较耗CPU,在高并发的情况下需要考虑CPU占用问题。还有一个问题,jwt生成的token比较长,可能需要考虑流量问题。
 

五、代码示例

JwtUtil.java

@Component
public class JwtUtil {

    private static UserRepository userRepository;

    @Autowired
    public JwtUtil(UserRepository userRepository) {
        JwtUtil.userRepository = userRepository;
    }

    public static final long EXPIRATION_TIME = 3600_000_000L; // 1000 hour
    static final String SECRET = "ThisIsASecret";
    static final String TOKEN_PREFIX = "Bearer";
    static final String HEADER_STRING = "Authorization";

    public static String generateToken(String username,Date generateTime) {
        HashMap<String, Object> map = new HashMap<>();
        //可以把任何安全的数据放到map里面
        map.put("username", username);
        map.put("generateTime",generateTime);
        String jwt = Jwts.builder()
                .setClaims(map)
                .setExpiration(new Date(generateTime.getTime() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
        return jwt;
    }

    /**
     * @param token
     * @return
     */
    public static Map<String,Object> validateToken(String token) {
        Map<String,Object> resp = new HashMap<String,Object>();
        if (token != null) {
            // 解析token
            try {
                Map<String, Object> body = Jwts.parser()
                        .setSigningKey(SECRET)
                        .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                        .getBody();
                String username = (String) (body.get("username"));
                Date generateTime = new Date((Long)body.get("generateTime"));

                if(username == null || username.isEmpty()){
                    resp.put("ERR_MSG",Constants.ERR_MSG_USERNAME_EMPTY);
                    return resp;
                }
                //账号在别处登录
                if(userRepository.findByUsername(username).getLastLoginTime().after(generateTime)){
                    resp.put("ERR_MSG",Constants.ERR_MSG_LOGIN_DOU);
                    return resp;
                }
                resp.put("username",username);
                resp.put("generateTime",generateTime);
                return resp;
            }catch (SignatureException | MalformedJwtException e) {
                // TODO: handle exception
                // don't trust the JWT!
                // jwt 解析错误
                resp.put("ERR_MSG",Constants.ERR_MSG_TOKEN_ERR);
                return resp;
            } catch (ExpiredJwtException e) {
                // TODO: handle exception
                // jwt 已经过期,在设置jwt的时候如果设置了过期时间,这里会自动判断jwt是否已经过期,如果过期则会抛出这个异常,我们可以抓住这个异常并作相关处理。
                resp.put("ERR_MSG",Constants.ERR_MSG_TOKEN_EXP);
                return resp;
            }
        }else {
            resp.put("ERR_MSG",Constants.ERR_MSG_TOKEN_EMPTY);
            return resp;
        }
    }
}

UserController.java

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserRepository userRepository;


    //注册或登录
    @RequestMapping("/login")
    @Transactional
    public UserResponse login(User user){

        String username = user.getUsername();
        String password = user.getPassword();
        //TODO  检验参数的完整性

        UserResponse userResponse = new UserResponse();
        User tUser = userRepository.findByUsername(username);
        //检验username是否存在
        user.setLastLoginTime(new Date());
        if(tUser!=null){
            //检验密码是否正确
            if(!tUser.getPassword().equals(password)) {
                userResponse.setErrorNum(Constants.ERR_NUM_PWD_ERR);
                userResponse.setErrorMsg(Constants.ERR_MSG_PWD_ERR);
                return userResponse;
            }
            userRepository.updateLastLoginTimeByUserName(user.getLastLoginTime(),username);

        }else {
            try {
                tUser = userRepository.save(user);
            } catch (Exception e) {
                userResponse.setErrorNum(Constants.ERR_NUM_SERVER_ERR);
                userResponse.setErrorMsg(Constants.ERR_MSG_SERVER_ERR);
                return userResponse;
            }
        }
        userResponse.setErrorNum(Constants.ERR_NUM_OK);
        userResponse.setErrorMsg(Constants.ERR_MSG_OK);
        userResponse.setUserName(username);
        userResponse.setUserId(tUser.getId());
        userResponse.setToken(JwtUtil.generateToken(username,user.getLastLoginTime()));

        return userResponse;
    }
}

HelloController.java

@RestController
public class HelloController {
    @RequestMapping("/hello")
    public Map login(HttpServletRequest request){
        String token = request.getParameter("token");
        return  JwtUtil.validateToken(token);
    }
}

猜你喜欢

转载自blog.csdn.net/huangpeigui/article/details/85620078