您的位置:首页 > 编程语言 > Java开发

Java实现基于token认证

2019-06-29 18:17 561 查看

Java实现基于token认证

随着互联网的不断发展,技术的迭代也非常之快。我们的用户认证也从刚开始的用户名密码转变到基于cookie的session认证,然而到了今天,这种认证已经不能满足与我们的业务需求了(分布式,微服务)。我们采用了另外一种认证方式:基于token的认证。

一、与cookie相比较的优势

  1. 支持跨域访问,将token置于请求头中,而cookie是不支持跨域访问的
  2. 无状态化,服务端无需存储token,只需要验证token信息是否正确即可,而session需要在服务端存储,一般是通过cookie中的sessionID在服务端查找对应的session
  3. 无需绑定到一个特殊的身份验证方案(传统的用户名密码登陆),只需要生成的token是符合我们预期设定的即可
  4. 更适用于移动端(Android,iOS,小程序等等),像这种原生平台不支持cookie,比如说微信小程序,每一次请求都是一次会话,当然我们可以每次去手动为他添加cookie
  5. 避免CSRF跨站伪造攻击,还是因为不依赖cookie
  6. 常适用于RESTful API,这样可以轻易与各种后端(java,.net,python…)相结合,去耦合

还有一些优势这里就不一一列举了。

二、基于JWT的token认证实现

JWT:JSON Web Token,其实token就是一段字符串,由三部分组成:Header,Payload,Signature。详细情况请自行百度,现在,上代码。

  1. 引入依赖,这里选用java-jwt,选择其他的依赖也可以

    <dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.3.0</version>
    </dependency>
  2. 实现签名方法

    /**
    * 生成签名,15min后过期
    *
    * @param username 用户名
    * @return 加密的token
    */
    public static String sign(String username, String userId) {
    try {
    // 过期时间
    Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
    // 私钥及加密算法
    Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
    // 设置头部信息
    Map<String, Object> header = new HashMap<>(2);
    header.put("typ", "JWT");
    header.put("alg", "HS256");
    // 附带username,userId信息,生成签名
    return JWT.create().withHeader(header).withClaim("loginName", username).withClaim("userId", userId)
    .withExpiresAt(date).sign(algorithm);
    } catch (UnsupportedEncodingException e) {
    return null;
    }
    }
    /**
    * 过期时间一天, TODO 正式运行时修改为15分钟
    */
    private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000;
    /**
    * token私钥
    */
    private static final String TOKEN_SECRET = "f26e587c28064d0e855e72c0a6a0e618";

    设置15分钟过期也是出于安全考虑,防止token被窃取,不过一般选择基于token认证,传输方式我们都应该选择https,这样别人无法抓取到我们的请求信息。这个私钥是非常重要的,加密解密都需要用到它,要设置的足够复杂并且不被盗取,我这里用的是一串uuid,加密方式HMAC256.

  3. 认证

    package com.joe.web;
    
    import com.joe.entity.ApiResponse;
    import com.joe.entity.User;
    import com.joe.enums.ApiResponseEnum;
    import com.joe.service.IUserService;
    import com.joe.util.ApiResponseUtil;
    import com.joe.util.JwtUtil;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import java.util.Map;
    
    /**
    * @author:joe
    * @date:2019/5/20 19:40
    */
    @Controller
    @RequestMapping("/")
    public class LoginController {
    
    @Autowired
    private IUserService userService;
    
    /**
    * 登陆接口
    *
    * @return token
    */
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    @ResponseBody
    public ApiResponse login(@RequestBody Map<String, String> map) {
    String loginName = map.get("loginName");
    String password = map.get("password");
    //身份验证是否成功
    boolean isSuccess = userService.checkUser(loginName, password);
    if (isSuccess) {
    User user = userService.getUserByLoginName(loginName);
    if (user != null) {
    //返回token
    String token = JwtUtil.sign(user.getName(), user.getId());
    if (token != null) {
    return ApiResponseUtil.getApiResponse(token);
    }
    }
    }
    //返回登陆失败消息
    return ApiResponseUtil.getApiResponse(ApiResponseEnum.LOGIN_FAIL);
    }
    }
  4. 配置拦截器

    package com.joe.interceptor;
    
    import com.alibaba.fastjson.JSONObject;
    import com.joe.entity.ApiResponse;
    import com.joe.enums.ApiResponseEnum;
    import com.joe.util.ApiResponseUtil;
    import com.joe.util.JwtUtil;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.PrintWriter;
    
    /**
    * 自定义token拦截器
    *
    * @author qiaokun
    * @date 2018/08/11
    */
    public class TokenInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    response.setCharacterEncoding("utf-8");
    String token = request.getHeader("access_token");
    //token不存在
    if (null != token) {
    //验证token是否正确
    boolean result = JwtUtil.verify(token);
    if (result) {
    return true;
    }
    }
    ApiResponse apiResponse = ApiResponseUtil.getApiResponse(ApiResponseEnum.AUTH_ERROR);
    responseMessage(response,response.getWriter(),apiResponse);
    return false;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    
    }
    
    /**
    * 返回信息给客户端
    *
    * @param response
    * @param out
    * @param apiResponse
    */
    private void responseMessage(HttpServletResponse response, PrintWriter out, ApiResponse apiResponse) {
    response.setContentType("application/json; charset=utf-8");
    out.print(JSONObject.toJSONString(apiResponse));
    out.flush();
    out.close();
    }
    }

    实现HandleInterceptor,重写preHandle方法,该方法是在每个请求之前触发执行,从request的头里面取出token,这里我们统一了存放token的键为accessToken,验证通过,放行,验证不通过,返回认证失败信息。

  5. 设置拦截器

    <mvc:interceptors>
    <mvc:interceptor>
    <mvc:mapping path="/**" />
    <mvc:exclude-mapping path="/login/"/>
    <bean class="com.joe.interceptor.TokenInterceptor"></bean>
    </mvc:interceptor>
    </mvc:interceptors>

    这里使用的是Spring的xml配置拦截器,放过认证接口。

  6. token解码方法

    /**
    * 校验token是否正确
    *
    * @param token 密钥
    * @return 是否正确
    */
    public static boolean verify(String token) {
    try {
    Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
    JWTVerifier verifier = JWT.require(algorithm).build();
    DecodedJWT jwt = verifier.verify(token);
    return true;
    } catch (Exception exception) {
    return false;
    }
    }
  7. 测试

    登录

    接口:localhost:8081/user

    post=>{
    //body
    "loginName":"zsj",
    "password":"123456"
    }
    {
    "errCode": 10000,
    "errMsg": "请求成功",
    "data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbk5hbWUiOiJqb2UiLCJleHAiOjE1NjE4ODYwODAsInVzZXJJZCI6IjIwMTkifQ.obShKP4jJjmpadxAP0KuAwHgdcjfgu_mACQbUNmNm1A"
    }
  8. 查询用户

    get=>{
    //header
    "access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbk5hbWUiOiJqb2UiLCJleHAiOjE1NjE4ODU1MzAsInVzZXJJZCI6IjIwMTkifQ.nNR2z_nY4_3ATllZqK21uUPcvYcCauWWdDiyV0xFA0A"
    }
    {
    "errCode": 10000,
    "errMsg": "请求成功",
    "data": {
    "id": "2019",
    "name": "joe",
    "age": 18
    }
    }
  • 获取token里携带的信息

    我们可以将一些常用的信息放入token中,比如用户登陆信息,可以方便我们的使用

    /**
    * 获得token中的信息无需secret解密也能获得
    *
    * @return token中包含的用户名
    */
    public static String getUsername(String token) {
    try {
    DecodedJWT jwt = JWT.decode(token);
    return jwt.getClaim("loginName").asString();
    } catch (JWTDecodeException e) {
    return null;
    }
    }
  • 代码的侵入性

    当你的代码引入了一个组件,导致其它代码或者设计,要做相应的更改以适应新组件。这样的情况我们就认为这个新组件具有侵入性。同时,这里又涉及到一个设计方面的概念,就是耦合性的问题。我们代码设计的思路是"高内聚,低耦合",为了实现这个思路,就必须降低代码的侵入性.

    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: