基于session的访问控制
-
应用场景:
一般用于前后端不分离的情况下
-
原理概述:
客户端的HTTP请求中携带sessionid(一般存放在cookie中),服务端根据请求中的sessionid找到内存中对应的session(session中存储着用户数据)
-
一种基于session的访问控制流程:
①客户端访问受保护页面
②服务端检查请求头中的sessionid(一般存放在cookie中)
③若未发现sessionid或sessionid无效,跳转到登录界面【若sessionid有效则跳转到⑧】
④客户端提交用户名与密码
⑤服务端检验成功,将用户信息写入session对象
⑥服务端生成sessionid并写入响应的Set-Cookie中(由tomcat完成)
⑦客户端再次访问受保护页面
⑧服务端通过请求的cookie中的sessionid获得对应session对象
⑨通过验证,允许访问受保护页面
-
缺陷:
如果搭建了多个服务器,虽然每个服务器都执行的是同样的业务逻辑,但是session数据是保存在服务器内存中的(不是共享的)。例如用户第一次访问的是服务器1,存储在cookie中的sessionid只能用于服务器1,当用户再次请求时可能访问的是另外一台服务器2,服务器2获取不到session信息,就判定用户没有登陆过。
基于token的访问控制
-
应用场景:
一般应用在前后端分离场景
-
原理概述:
根据用户名、时间戳、过期时间、发行者等信息生成一个加密的字符串token返回给客户端,客户端会保存这个字符串(Local Storage、cookie等),并在请求时携带该字符串,而服务端不会保存这个字符串,只会对请求中携带的字符串进行解密,并判断该token是否有效,若有效则可从解密后的token中提取数据。即使有了多台服务器,服务器也只是做了token的解密和用户数据的查询,它不需要在服务端去保留用户的认证信息或者会话信息,这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利,解决了session扩展性的弊端。
-
一种基于token的访问控制流程:
-
详细介绍:
-
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
-
-
另一种常见token的实现机制是使用token+redis,以此确定token是否有效,并保证一个用户只有一个token可用,还可保证一个用户只能同时在线一台机器。详细请参考前后端分离之用户登录状态管理和校验
-
-
基于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; } }
-
-
简单应用:
-
登录、登出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; } }
-