REST(Representational State Transfer)是一種軟件架構風格。它將服務端的信息和功能等全部事物統稱爲資源,客戶端的請求實際就是對資源進行操做,它的主要特色有: – 每個資源都會對應一個獨一無二的url – 客戶端經過HTTP的GET、POST、PUT、DELETE請求方法對資源進行查詢、建立、修改、刪除操做 – 客戶端與服務端的交互必須是無狀態的html
關於RESTful的詳細介紹能夠參考這篇文章,在此就不浪費時間直接進入正題了。java
網站應用通常使用Session進行登陸用戶信息的存儲及驗證,而在移動端使用Token則更加廣泛。它們之間並無太大區別,Token比較像是一個更加精簡的自定義的Session。Session的主要功能是保持會話信息,而Token則只用於登陸用戶的身份鑑權。因此在移動端使用Token會比使用Session更加簡易而且有更高的安全性,同時也更加符合RESTful中無狀態的定義。mysql
服務端生成的Token通常爲隨機的非重複字符串,根據應用對安全性的不一樣要求,會將其添加時間戳(經過時間判斷Token是否被盜用)或url簽名(經過請求地址判斷Token是否被盜用)後加密進行傳輸。在本文中爲了演示方便,僅是將User Id與Token以」_」進行拼接。git
/** * Token的Model類,能夠增長字段提升安全性,例如時間戳、url簽名 * @author ScienJus * @date 2015/7/31. */ public class TokenModel { //用戶id private long userId; //隨機生成的uuid private String token; public TokenModel(long userId, String token) { this.userId = userId; this.token = token; } public long getUserId() { return userId; } public void setUserId(long userId) { this.userId = userId; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } }
Redis是一個Key-Value結構的內存數據庫,用它維護User Id和Token的映射表會比傳統數據庫速度更快,這裏使用spring-Data-redis封裝的TokenManager對Token進行基礎操做:github
/** * 對token進行操做的接口 * @author ScienJus * @date 2015/7/31. */ public interface TokenManager { /** * 建立一個token關聯上指定用戶 * @param userId 指定用戶的id * @return 生成的token */ public TokenModel createToken(long userId); /** * 檢查token是否有效 * @param model token * @return 是否有效 */ public boolean checkToken(TokenModel model); /** * 從字符串中解析token * @param authentication 加密後的字符串 * @return */ public TokenModel getToken(String authentication); /** * 清除token * @param userId 登陸用戶的id */ public void deleteToken(long userId); } /** * 經過Redis存儲和驗證token的實現類 * @author ScienJus * @date 2015/7/31. */ @Component public class RedisTokenManager implements TokenManager { private RedisTemplate redis; @Autowired public void setRedis(RedisTemplate redis) { this.redis = redis; //泛型設置成Long後必須更改對應的序列化方案 redis.setKeySerializer(new JdkSerializationRedisSerializer()); } public TokenModel createToken(long userId) { //使用uuid做爲源token String token = UUID.randomUUID().toString().replace("-", ""); TokenModel model = new TokenModel(userId, token); //存儲到redis並設置過時時間 redis.boundValueOps(userId).set(token, Constants.TOKEN_EXPIRES_HOUR, TimeUnit.HOURS); return model; } public TokenModel getToken(String authentication) { if (authentication == null || authentication.length() == 0) { return null; } String[] param = authentication.split("_"); if (param.length != 2) { return null; } //使用userId和源token簡單拼接成的token,能夠增長加密措施 long userId = Long.parseLong(param[0]); String token = param[1]; return new TokenModel(userId, token); } public boolean checkToken(TokenModel model) { if (model == null) { return false; } String token = redis.boundValueOps(model.getUserId()).get(); if (token == null || !token.equals(model.getToken())) { return false; } //若是驗證成功,說明此用戶進行了一次有效操做,延長token的過時時間 redis.boundValueOps(model.getUserId()).expire(Constants.TOKEN_EXPIRES_HOUR, TimeUnit.HOURS); return true; } public void deleteToken(long userId) { redis.delete(userId); } }
RESTful中全部請求的本質都是對資源進行CRUD操做,因此登陸和退出登陸也能夠抽象爲對一個Token資源的建立和刪除,根據該想法建立Controller:web
/** * 獲取和刪除token的請求地址,在Restful設計中其實就對應着登陸和退出登陸的資源映射 * @author ScienJus * @date 2015/7/30. */ @RestController @RequestMapping("/tokens") public class TokenController { @Autowired private UserRepository userRepository; @Autowired private TokenManager tokenManager; @RequestMapping(method = RequestMethod.POST) public ResponseEntity login(@RequestParam String username, @RequestParam String password) { Assert.notNull(username, "username can not be empty"); Assert.notNull(password, "password can not be empty"); User user = userRepository.findByUsername(username); if (user == null || //未註冊 !user.getPassword().equals(password)) { //密碼錯誤 //提示用戶名或密碼錯誤 return new ResponseEntity<>(ResultModel.error(ResultStatus.USERNAME_OR_PASSWORD_ERROR), HttpStatus.NOT_FOUND); } //生成一個token,保存用戶登陸狀態 TokenModel model = tokenManager.createToken(user.getId()); return new ResponseEntity<>(ResultModel.ok(model), HttpStatus.OK); } @RequestMapping(method = RequestMethod.DELETE) @Authorization public ResponseEntity logout(@CurrentUser User user) { tokenManager.deleteToken(user.getId()); return new ResponseEntity<>(ResultModel.ok(), HttpStatus.OK); } }
這個Controller中有兩個自定義的註解分別是@Authorization
和@CurrentUser
,其中@Authorization
用於表示該操做須要登陸後才能進行:redis
這裏使用Spring的攔截器完成這個功能,該攔截器會檢查每個請求映射的方法是否有@Authorization
註解,並使用TokenManager驗證Token,若是驗證失敗直接返回401狀態碼(未受權):spring
/** * 自定義攔截器,判斷這次請求是否有權限 * @author ScienJus * @date 2015/7/30. */ @Component public class AuthorizationInterceptor extends HandlerInterceptorAdapter { @Autowired private TokenManager manager; public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //若是不是映射到方法直接經過 if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); //從header中獲得token String authorization = request.getHeader(Constants.AUTHORIZATION); //驗證token TokenModel model = manager.getToken(authorization); if (manager.checkToken(model)) { //若是token驗證成功,將token對應的用戶id存在request中,便於以後注入 request.setAttribute(Constants.CURRENT_USER_ID, model.getUserId()); return true; } //若是驗證token失敗,而且方法註明了Authorization,返回401錯誤 if (method.getAnnotation(Authorization.class) != null) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } return true; } }
@CurrentUser
註解定義在方法的參數中,表示該參數是登陸用戶對象。這裏一樣使用了Spring的解析器完成參數注入:sql
/** * 在Controller的方法參數中使用此註解,該方法在映射時會注入當前登陸的User對象 * @author ScienJus * @date 2015/7/31. */ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface CurrentUser { } /** * 增長方法注入,將含有CurrentUser註解的方法參數注入當前登陸用戶 * @author ScienJus * @date 2015/7/31. */ @Component public class CurrentUserMethodArgumentResolver implements HandlerMethodArgumentResolver { @Autowired private UserRepository userRepository; @Override public boolean supportsParameter(MethodParameter parameter) { //若是參數類型是User而且有CurrentUser註解則支持 if (parameter.getParameterType().isAssignableFrom(User.class) && parameter.hasParameterAnnotation(CurrentUser.class)) { return true; } return false; } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { //取出鑑權時存入的登陸用戶Id Long currentUserId = (Long) webRequest.getAttribute(Constants.CURRENT_USER_ID, RequestAttributes.SCOPE_REQUEST); if (currentUserId != null) { //從數據庫中查詢並返回 return userRepository.findOne(currentUserId); } throw new MissingServletRequestPartException(Constants.CURRENT_USER_ID); } }
本文的完整示例程序已發佈在個人Github上,能夠下載並按照readme.md的流程進行操做。數據庫