上一篇( 一个六年经验的python后端是怎么学习用java写API的(5) Service 和 google 依赖注入 )
实现了依赖注入之后就可以方便的实现各种API的业务逻辑了,下一部的问题就在于权限,我们知道大部分的系统API并不是开放的,需要基本的用户体系(注册、登录、购买、会员、不同的role等等),例如管理员能看到CMS,登录用户才能查看文章详情等等。
parrot tag: auth-and-token
直接看下廖雪峰的文章 https://www.ruanyifeng.com/bl... 。
简单的说就是用户登录后,客户端(web、iOS、Android)会拿到登录成功返回的一个token,请求其他接口时把token带在Header里面而不用sessionId的策略,比如django restframe work 的 TokenAuthentication,python oauth2 provider accesstoken 等实现。目前基本算业内的通用方案了,比如找下微信开发者文档、微博开发者文档会看到具体的例子。
https://www.dropwizard.io/en/...
https://github.com/ToastShama...│ ├── auth │ │ ├── CustomJWTAuthFilter.java │ │ ├── NoAuth.java │ │ ├── ParrotSecurityContext.java │ │ ├── UserAuthenticationDynamicFeature.java │ │ ├── UserAuthenticationFilter.java │ │ ├── UserAuthenticator.java │ │ ├── UserAuthorizer.java │ │ └── hasher │ │ ├── PBKDF2PasswordHasher.java │ │ └── PasswordHasher.java │ ├── bundles │ │ ├── AuthBundle.java │ │ ├── CorsBundle.java │ │ ├── GuiceBundle.java │ │ └── MysqlBundle.java
根据dropwizard的文档,主要需要实现下面的类和方法
Authenticator.authenticate 通过token的subject拿到用户唯一标识username
public Optional<User> authenticate(JsonWebToken token) { final JsonWebTokenValidator expiryValidator = getValidator(); try { expiryValidator.validate(token); } catch (TokenExpiredException e) { throw e; } User user = userMapper.selectByUsername(token.claim().subject()); return Optional.fromNullable(user); }
JsonWebTokenServiceImpl.tokenize 给用户一个可用的token
@Override public JsonWebToken tokenize(User user) { return JsonWebToken.builder() .header(JsonWebTokenHeader.HS512()) .claim(JsonWebTokenClaim.builder() .subject(user.getUsername()) .issuedAt(DateTime.now()) .expiration(DateTime.now().plusHours(DEFAULT_SESSION_EXPIRATION_HOURS)) .build()) .build(); }
UserAuthorizer.authorize 可以自己定义某些permission字符串实现对应方法,这里我没用到这么细的权限,用户那直接根据用户是否超级管理员和是否有效做了判断
public boolean authorize(User user, String permission) { return user.hasPermission(permission); }
user.hasPermission
public boolean hasPermission(String permission) { if (isSuperUser) { return true; } if (isActive) { return true; } return false; }
Login API, 不需要登录的接口添加NoAuth,这个东西类似 django restframework 的 rest_framework.permissions 里面的 AllowAny,默认是需要登录的,这样需要登录的接口在param里面加上 @Auth user 即可拿到登录后的用户
@POST @Consumes(APPLICATION_JSON) @NoAuth @Path("/login") public MetaMapperResponse login(LoginRequest loginRequest, @Context HttpServletRequest request) throws InvalidKeySpecException, NoSuchAlgorithmException { Optional<User> optionalUser = userService.login(loginRequest.getUsername(), loginRequest.getPassword()); if (!optionalUser.isPresent()){ throw new NotAuthorizedException("Wrong username or password"); } User user = optionalUser.get(); String token = jsonWebTokenService.tokenizeAndSign(user); UserSerializer serializer = UserSerializer.build(user); MetaMapperResponse response = new MetaMapperResponse(); response.putMeta("token", token); response.setData(serializer); return response; }
因为用了django的用户系统(是在是懒,直接用django admin的cms管理用户),django user的密码加密默认采用了pbkdf2_sha256这个加密算法。
django/contrib/auth/hashers.py PBKDF2PasswordHasher,需要把这个翻译成java
class PBKDF2PasswordHasher(BasePasswordHasher): """ Secure password hashing using the PBKDF2 algorithm (recommended) Configured to use PBKDF2 + HMAC + SHA256. The result is a 64 byte binary string. Iterations may be changed safely but you must rename the algorithm if you change SHA256. """ algorithm = "pbkdf2_sha256" iterations = 36000 digest = hashlib.sha256 def encode(self, password, salt, iterations=None): assert password is not None assert salt and '$' not in salt if not iterations: iterations = self.iterations hash = pbkdf2(password, salt, iterations, digest=self.digest) hash = base64.b64encode(hash).decode('ascii').strip() return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash) def verify(self, password, encoded): algorithm, iterations, salt, hash = encoded.split('$', 3) assert algorithm == self.algorithm encoded_2 = self.encode(password, salt, int(iterations)) return constant_time_compare(encoded, encoded_2)
自己翻译了一会儿实现了,结果发现之前有人写过, https://gist.github.com/spapa... ,直接抄过来了,只不过需要注意我用的版本是django 1.11 默认他的iterations=36000,需要对应修改一下,这样就可以直接用django的用户系统做登录了。
UserServiceImpl 这样使用
public Optional<User> login(String username, String password) throws InvalidKeySpecException, NoSuchAlgorithmException { User user = userMapper.selectByUsername(username); Boolean correct = passwordHasher.checkPassword(password, user.getPassword()); if (correct){ return Optional.ofNullable(user); } return Optional.empty(); }
之前写过的查看文章详情接口,不加@NoAuth即为需要token,参数里面添加@Auth User user即可拿到登录的用户。
@Path("/{id}/secret") @GET @Timed public MetaMapperResponse getSecretArticle(@Auth User user, @NotNull @PathParam("id") Integer articleId) { MetaMapperResponse response = new MetaMapperResponse(); Boolean isActive = true; Article article = articleService.get(articleId, isActive); response.setData(article); return response; }
请求时 header中添加 key:Authorization, value: Bearer空格token
key: Authorization value: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJleHAiOjE1ODUxMjg1MjcsImlhdCI6MTU4NDUyMzcyNywic3ViIjoieWFuZ3lhbmcifQ.F8HKeA2qEz3btJvsM6vvP0T3i0E-dk-FEB-RZzmNBy09xO3VEAXPXzRxIaq6/_18XzZOeKlXYnmndEkgiEgVBFA
即可返回
{ "meta": {}, "data": { "id": 4, "cover": "http://cdn.reworkplan.com/6bf32d3229a4.jpg", "title": "【解局】湖北多地开始实施“战时管制”,为什么?", "description": "不必恐慌,再坚持忍耐一下", "is_active": true, "created": 1581754687304 } }
token不对则 401 Credentials are required to access this resource.
这个系列大概率完结了,因为剩下的东西在于具体的业务权限了,对于项目架构的东西基本完了。可能还需要做的是:
上面的这些问题可能需要边写边重构,看是否有必要在写对应的文章好了。之后可能会去学一下react、react-router、react-redux做一下前端。