基于session、token的简单访问控制

基于session的访问控制

  1. 应用场景:

    一般用于前后端不分离的情况下

  2. 原理概述:

    客户端的HTTP请求中携带sessionid(一般存放在cookie中),服务端根据请求中的sessionid找到内存中对应的session(session中存储着用户数据)

  3. 一种基于session的访问控制流程:

    ①客户端访问受保护页面

    ②服务端检查请求头中的sessionid(一般存放在cookie中)

    ③若未发现sessionid或sessionid无效,跳转到登录界面【若sessionid有效则跳转到⑧】

    ④客户端提交用户名与密码

    ⑤服务端检验成功,将用户信息写入session对象

    ⑥服务端生成sessionid并写入响应的Set-Cookie中(由tomcat完成)

    ⑦客户端再次访问受保护页面

    ⑧服务端通过请求的cookie中的sessionid获得对应session对象

    ⑨通过验证,允许访问受保护页面

  4. 缺陷:

    如果搭建了多个服务器,虽然每个服务器都执行的是同样的业务逻辑,但是session数据是保存在服务器内存中的(不是共享的)。例如用户第一次访问的是服务器1,存储在cookie中的sessionid只能用于服务器1,当用户再次请求时可能访问的是另外一台服务器2,服务器2获取不到session信息,就判定用户没有登陆过。

基于token的访问控制

  1. 应用场景:

    一般应用在前后端分离场景

  2. 原理概述:

    根据用户名、时间戳、过期时间、发行者等信息生成一个加密的字符串token返回给客户端,客户端会保存这个字符串(Local Storage、cookie等),并在请求时携带该字符串,而服务端不会保存这个字符串,只会对请求中携带的字符串进行解密,并判断该token是否有效,若有效则可从解密后的token中提取数据。即使有了多台服务器,服务器也只是做了token的解密和用户数据的查询,它不需要在服务端去保留用户的认证信息或者会话信息,这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利,解决了session扩展性的弊端。

  3. 一种基于token的访问控制流程:
    基于session、token的简单访问控制

  4. 详细介绍:

    • JWT,全写JSON Web Token,是一种token实现机制,它不用查库或者少查库,直接在服务端进行校验,并且不用查库,因为用户的信息及加密信息在第二部分payload和第三部分签证中已经生成,只要在服务端进行校验就行,并且校验也是JWT自己实现的。

      JWT字符串是一段加密的JSON字符串,一个完整的JWT=头信息+有效载荷+签名这三部分组成,中间的加号换成.进行分割

      • Header头部: Token类型和加密算法。加密算法常见的有MD5、SHA、HMAC( Hash Message Authentication Code)。

      • PayLoad负载:存放有效信息,包括

        • 标准的声明,类似开发语言总的关键字。包括
          • iss(Issuser) - 签发者
          • sub Subject 面向主体
          • aud Audience 接收方
          • exp Expiration time 过期时间戳
          • nbf Not Before, 开始生效时间戳
          • iat(Issued at) 签发时间
          • jti(JWT ID): 唯一标识
        • 公共的声明: 一般添加业务相关的必要信息,因为可解密,不建议敏感信息。
        • 私有的声明:提供者和消费者共同定义的声明,Base64对称解密,不建议敏感信息
      • Signature签证:签证信息包括三部分

        • Base64加密的header
        • Base64加密的payload
        • secret-密钥

        最后的Signature为使用header中声明的加密算法对Header和payload的加密连接字符串进行加盐secret组合加密得到的字符串。密钥保存在服务端,服务端根据密钥进行解密验证。

      一个JWT实例:

      • header:

        {
          'typ': 'JWT',
          'alg': 'HS256'
        }
        
      • payload:

        {
          "sub": "1234567890",
          "name": "John Doe",
          "admin": true
        }
        
      • signature:

        encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload)
        signature = HMACSHA256(encodedString, 'secret');
        
      • 完整的jwt

        eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
        
        #eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9:base64加密后的header
        #eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9:base64加密后的payload
        #TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ:signature
        

      基于session、token的简单访问控制

    • 另一种常见token的实现机制是使用token+redis,以此确定token是否有效,并保证一个用户只有一个token可用,还可保证一个用户只能同时在线一台机器。详细请参考前后端分离之用户登录状态管理和校验

  5. 基于java-jwt的token操作

    • 依赖

      <dependency>
          <groupId>com.auth0</groupId>
          <artifactId>java-jwt</artifactId>
          <version>3.8.2</version>
      </dependency>
      
    • 产生token

      // JWT的header部分,该map可以是空的,因为有默认值{"alg":HS256,"typ":"JWT"}
      Map<String, Object> map = new HashMap<>();
      final String TOKEN_SECRET = "111111";	// 密钥
      String token = JWT.create()
          		.withHeader(map)	// 添加头部
                 	.withClaim("userid",userDB.getId())	// 添加payload
          		.withClaim("username",userDB.getUsername())
          		.withExpiresAt(newDate(System.currentTimeMillis()))	// 设置过期时间
                	.sign(Algorithm.HMAC256(TOKEN_SECRET));	// 使用HMAC算法加盐
      
    • 解密token,验证Token是否有效并获取负载信息

      Result result = new Result();	// Result类型自定义
      try {
          /* 验证jwt是否可用(在有效期内、格式正确...),不可用会抛出异常 */
          Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
          JWTVerifier verifier = JWT.require(algorithm).build();
          DecodedJWT jwt = verifier.verify(token);
          
          /* 获取JWT中的数据,注意数据类型一定要与添加进去的数据类型一致,否则取不到数据 */
          System.out.println(decodedJWT.getClaim("userid").asInt());
          System.out.println(decodedJWT.getClaim("username").asString());
          System.out.println(decodedJWT.getExpiresAt());
          
          // 设置result状态码为成功,将数据写入result
      }catch (Exception e){
          // 设置result状态码为错误
      }
      return result;
      
    • token工具类示例(payload为userid)

      package pers.zero.utils;
      
      import com.auth0.jwt.JWT;
      import com.auth0.jwt.JWTVerifier;
      import com.auth0.jwt.algorithms.Algorithm;
      import com.auth0.jwt.interfaces.DecodedJWT;
      import pers.zero.common.Result;
      
      import java.util.Date;
      import java.util.HashMap;
      import java.util.Map;
      
      /* 设计的不太好,待优化 */
      public final class UserTokenUtils {
      
          private UserTokenUtils(){}
      
          private static final int MIN = 60*1000;
      
          private static final long EXPIRE_TIME;      // 有效时间,单位ms
          private static final String TOKEN_SECRET;   // token密钥
          private static final Algorithm algorithm;   // 加密算法
          private static final Map<String,Object> tokenHeader;    // token header
      
          static {
              EXPIRE_TIME = 30*MIN;
      
              TOKEN_SECRET = "ASdxxzdawsqzxxda";
      
              algorithm = Algorithm.HMAC256(TOKEN_SECRET);
      
              tokenHeader = new HashMap<>();
              tokenHeader.put("alg","HS256");
              tokenHeader.put("typ","JWT");
          }
      
          /**
           * use userid to create token,token's EXPIRE_TIME is 30 min
           * @param userid : userid
           * @return : jwt string
           */
          public static String createJWT(int userid){
              String jwt = JWT.create()
                              .withHeader(tokenHeader)
                              .withClaim("userid",userid)
                              .withExpiresAt(new Date(System.currentTimeMillis()+EXPIRE_TIME))
                              .sign(algorithm);
              return jwt;
          }
      
          /**
           * use userid to create token
           * @param userid : userid
           * @param expireTime : token's valid time , (min)
           * @return : jwt string
           */
          public static String createJWT(int userid,int expireTime){
              String jwt = JWT.create()
                      .withHeader(tokenHeader)
                      .withClaim("userid",userid)
                      .withExpiresAt(new Date(System.currentTimeMillis()+expireTime*MIN))
                      .sign(algorithm);
              return jwt;
          }
      
      
          /**
           * getResult from JWT 
           * @return : Result<Map<String,Object>>
                       if jwt is valid,resultCode is 200,and resultData is a Map {"userid":userid}
                       if jwt is invalid,resultCode is 401,and resultMessage is the error info
           */
          public static Result<Map<String,Object>> getResult(String token) {
              Result<Map<String,Object>> result = new Result<>();
              try {
                  JWTVerifier jwtVerifier = JWT.require(algorithm).build();
                  DecodedJWT decodedJWT = jwtVerifier.verify(token);
      
                  int userid = decodedJWT.getClaim("userid").asInt();
                  Map<String,Object> data = new HashMap<>();
                  data.put("userid",userid);
      
                  result.setResultCode(200);
                  result.setData(data);
                  result.setMessage("解码成功");
              } catch (Exception err){
                  result.setResultCode(401);
                  result.setMessage(err.getMessage());
              }
              return null;
          }
      
      }
      
  6. 简单应用:

    • 登录、登出API:

      @RestController
      @RequestMapping("/user")
      public class UserController {
      
          @Autowired
          UserService userService;
      
          /**
           * user login API
           * @apiNote : POST /user/session with params{username,password} [application/json]
           * @response :
                  success ~ {code:200 , set-cookie:"login_token=xxx", data:username}
                  error ~ {code:401 , message:"用户名或密码错误"}
           */
          @PostMapping(value = "/session",produces = {"application/json;charset=UTF-8"})
          public String login(HttpServletRequest request,HttpServletResponse response) throws IOException {
              // 预处理
              Map<String,String> requestDataMap;
              try {
                  BufferedReader streamReader = new BufferedReader(new InputStreamReader(request.getInputStream(), "UTF-8"));
                  StringBuilder responseStrBuilder = new StringBuilder();
                  String inputStr;
                  while ((inputStr = streamReader.readLine()) != null){
                      responseStrBuilder.append(inputStr);
                  }
                  requestDataMap = (Map<String,String>) JSONObject.parse(responseStrBuilder.toString());
              } catch (IOException e) {
                  e.printStackTrace();
                  response.sendError(500,"输入输出异常");
                  return "";
              }
              User user = new User();
              user.setUsername(requestDataMap.get("username"));
              user.setPassword(requestDataMap.get("password"));
              
              // 重点部分
              Result<Integer> result = userService.login(user);
              if(result.getResultCode()==GeneralResultCode.SUCCESS_CODE){
                  String loginToken = UserTokenUtil.createJWT(result.getData(),120); 
                  CookieUtil.setCookieUnderThisApp(request,response,
                          CookieConfigs.LOGIN_TOKEN_COOKIE_NEAM,loginToken,120);
                  Map<String,String> userInfoMap = new HashMap<>();
                  userInfoMap.put("username",user.getUsername());
                  return new ObjectMapper().writeValueAsString(userInfoMap);
              }else if(result.getResultCode()==GeneralResultCode.ERROR_CODE){
                  try {
                      response.sendError(401,"用户名或密码错误");
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
              return "";
          }
      
          /**
           * user logout API
           * @apiNote : DELETE /user/session
           */
          @DeleteMapping(value = "/session")
          public void logout(HttpServletRequest request,HttpServletResponse response){
              CookieUtil.deleteCookieUnderThisApp(request,response,
                      CookieConfigs.LOGIN_TOKEN_COOKIE_NEAM); // delete login_token cookie
          }
      
      }
      
    • 通过拦截器判断访问权限:

      public class UserLoginInterceptor implements HandlerInterceptor {
      
          /**
           * 检测到无效token则拦截请求并产生401响应,否则将token中的userid存入request并放行
           * @return : if user had login return true,else false
           * @throws Exception
           */
          @Override
          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
              Map<String, String> cookieMap = CookieUtil.getCookieMap(request);
              String token = cookieMap.get(CookieConfigs.LOGIN_TOKEN_COOKIE_NEAM);
              //decodeJWT(null)不会报错,Result{resultCode=-1, message='null', data=null}
              Result<Map<String, Object>> result = UserTokenUtil.decodeJWT(token);
              if (GeneralResultCode.ERROR_CODE==result.getResultCode()){
                  response.sendError(401,"当前账户未登录或会话失效,请重新登录");
                  return false;
              }else if(GeneralResultCode.SUCCESS_CODE ==result.getResultCode()){
                  int userid = (int) result.getData().get("userid");
                  request.setAttribute("userid",userid);
                  return true;
              }
      
              return false;
          }
      
      }
      

参考

上一篇:退出登录


下一篇:miaosha2:高并发抢购方案