设计思路
- 用户发出登录请求,带着用户名和密码到服务器进行验证,服务器验证成功就在后台生成一个token返回给客户端
- 客户端将token存储到cookie中,服务端将token存储到redis中,可以设置存储token的有效期。
- 后续客户端的每次请求资源都必须携带token,服务端接收到请求首先校验是否携带token,以及token是否和redis中的匹配,若不存在或不匹配直接拦截返回错误信息(如未认证)。
-
token管理:生成、校验、解析、删除
-
token:这里使用userId_UUID的形式
-
有效期:使用Redis key有效期设置(每次操作完了都会更新延长有效时间)
-
销毁token:删除Redis中key为userId的内容
-
token存储:客户端(Cookie)、服务端(Redis)
-
Cookie的存取操作(jquery.cookie插件)
-
Redis存取(StringRedisTemplate)
实现
【Redis操作类】
package com.bpf.tokenAuth.utils; import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; @Component public class RedisClient { public static final long TOKEN_EXPIRES_SECOND = 1800; @Autowired private StringRedisTemplate redisTpl; /** * 向redis中设值 * @param key 使用 a:b:id的形式在使用rdm进行查看redis情况时会看到分层文件夹的展示形式,便于管理 * @param value * @return */ public boolean set(String key, String value) { boolean result = false; try { redisTpl.opsForValue().set(key, value); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 向redis中设置,同时设置过期时间 * @param key * @param value * @param time * @return */ public boolean set(String key, String value, long time) { boolean result = false; try { redisTpl.opsForValue().set(key, value); expire(key, time); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 获取redis中的值 * @param key * @return */ public String get(String key) { String result = null; try { result = redisTpl.opsForValue().get(key); } catch (Exception e) { e.printStackTrace(); } return result; } /** * 设置key的过期时间 * @param key * @param time * @return */ public boolean expire(String key, long time) { boolean result = false; try { if(time > 0) { redisTpl.expire(key, time, TimeUnit.SECONDS); result = true; } } catch (Exception e) { e.printStackTrace(); } return result; } /** * 根据key删除对应value * @param key * @return */ public boolean remove(String key) { boolean result = false; try { redisTpl.delete(key); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } }
【Token管理类】
package com.bpf.tokenAuth.utils.token; import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.bpf.tokenAuth.utils.RedisClient; @Component public class RedisTokenHelp implements TokenHelper { @Autowired private RedisClient redisClient; @Override public TokenModel create(Integer id) { String token = UUID.randomUUID().toString().replace("-", ""); TokenModel mode = new TokenModel(id, token); redisClient.set(id == null ? null : String.valueOf(id), token, RedisClient.TOKEN_EXPIRES_SECOND); return mode; } @Override public boolean check(TokenModel model) { boolean result = false; if(model != null) { String userId = model.getUserId().toString(); String token = model.getToken(); String authenticatedToken = redisClient.get(userId); if(authenticatedToken != null && authenticatedToken.equals(token)) { redisClient.expire(userId, RedisClient.TOKEN_EXPIRES_SECOND); result = true; } } return result; } @Override public TokenModel get(String authStr) { TokenModel model = null; if(StringUtils.isNotEmpty(authStr)) { String[] modelArr = authStr.split("_"); if(modelArr.length == 2) { int userId = Integer.parseInt(modelArr[0]); String token = modelArr[1]; model = new TokenModel(userId, token); } } return model; } @Override public boolean delete(Integer id) { return redisClient.remove(id == null ? null : String.valueOf(id)); } }
【拦截器逻辑】
package com.bpf.tokenAuth.interceptor; import java.lang.reflect.Method; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import com.bpf.tokenAuth.annotation.NoneAuth; import com.bpf.tokenAuth.constant.NormalConstant; import com.bpf.tokenAuth.entity.JsonData; import com.bpf.tokenAuth.utils.JsonUtils; import com.bpf.tokenAuth.utils.token.TokenHelper; import com.bpf.tokenAuth.utils.token.TokenModel; @Component public class LoginInterceptor extends HandlerInterceptorAdapter { @Autowired private TokenHelper tokenHelper; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println(11); // 如果不是映射到方法直接通过 if (!(handler instanceof HandlerMethod)) { return true; } //如果被@NoneAuth注解代表不需要登录验证,直接通过 HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); if(method.getAnnotation(NoneAuth.class) != null) return true; //token验证 String authStr = request.getHeader(NormalConstant.AUTHORIZATION); TokenModel model = tokenHelper.get(authStr); //验证通过 if(tokenHelper.check(model)) { request.setAttribute(NormalConstant.CURRENT_USER_ID, model.getUserId()); return true; } //验证未通过 response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); response.getWriter().write(JsonUtils.obj2String(JsonData.buildError(401, "权限未认证"))); return false; } }
【登录逻辑】
package com.bpf.tokenAuth.controller; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.bpf.tokenAuth.annotation.NoneAuth; import com.bpf.tokenAuth.constant.MessageConstant; import com.bpf.tokenAuth.constant.NormalConstant; import com.bpf.tokenAuth.entity.JsonData; import com.bpf.tokenAuth.entity.User; import com.bpf.tokenAuth.enums.HttpStatusEnum; import com.bpf.tokenAuth.mapper.UserMapper; import com.bpf.tokenAuth.utils.token.TokenHelper; import com.bpf.tokenAuth.utils.token.TokenModel; @RestController @RequestMapping("/token") public class TokenController { @Autowired private UserMapper userMapper; @Autowired private TokenHelper tokenHelper; @NoneAuth @GetMapping public Object login(String username, String password) { User user = userMapper.findByName(username); if(user == null || !user.getPassword().equals(password)) { return JsonData.buildError(HttpStatusEnum.NOT_FOUND.getCode(), MessageConstant.USERNAME_OR_PASSWORD_ERROR); } //用户名密码验证通过后,生成token TokenModel model = tokenHelper.create(user.getId()); return JsonData.buildSuccess(model); } @DeleteMapping public Object logout(HttpServletRequest request) { Integer userId = (Integer) request.getAttribute(NormalConstant.CURRENT_USER_ID); if(userId != null) { tokenHelper.delete(userId); } return JsonData.buildSuccess(); } }
测试
【login.html】
<!DOCTYPE html> <html> <head> <title>Login</title> <link rel="stylesheet" href="../res/css/login.css"> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script> <script src="https://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.js"></script> </head> <body> <form> <input type="text" name="username" id="username"> <input type="password" name="password" id="password"> </form> <input type="button" value="Login" onclick="login()"> </body> <script type="text/javascript"> function login(){ $.ajax({ url: "/tokenAuth/token", dataType: "json", data: {'username':$("#username").val(), 'password':$("#password").val()}, type:"GET", success:function(res){ console.log(res); if(res.code == 200){ var authStr = res.data.userId + "_" + res.data.token; //把生成的token放在cookie中 $.cookie("authStr", authStr); window.location.href = "index.html"; }else alert(res.msg); } }); } </script> </html>
【index.html】
<!DOCTYPE html> <html> <head> <title>Index</title> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script> <script src="https://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.js"></script> </head> <body> <input type="button" value="Get" onclick="get()"> <input type="button" value="logout" onclick="logout()"> </body> <script type="text/javascript"> function get(){ $.ajax({ url: "/tokenAuth/user/bpf", dataType: "json", type:"GET", beforeSend: function(request) { //将cookie中的token信息放于请求头中 request.setRequestHeader("authStr", $.cookie('authStr')); }, success:function(res){ console.log(res); } }); } function logout(){ $.ajax({ url: "/tokenAuth/token", dataType: "json", type:"DELETE", beforeSend: function(request) { //将cookie中的token信息放于请求头中 request.setRequestHeader("authStr", $.cookie('authStr')); }, success:function(res){ console.log(res); } }); } </script> </html>
测试环境中两个页面login.html和index.html均当做静态资源处理
【未登录状态】
- 首先再未登录状态下直接访问index页面http://localhost:8080/tokenAuth/page/index.html
- 点击get按钮获取数据,由于没有携带token导致认证失败
【登录状态】
- 访问登录网站http://localhost:8080/tokenAuth/page/login.html,输入username和password进行点击Login按钮登录
-
登录成功并跳转到index页面,并且生成cookie,这里没有设置cookie有效期,默认关闭浏览器失效
再次点击get按钮请求数据,请求成功
点击logout按钮销毁登录状态,然后再次请求数据