描述
上一篇(一个六年经验的python后端是怎么学习用java写API的(5) Service 和 google 依赖注入)
实现了依赖注入之后就可以方便的实现各种API的业务逻辑了,下一部的问题就在于权限,我们知道大部分的系统API并不是开放的,需要基本的用户体系(注册、登录、购买、会员、不同的role等等),例如管理员能看到CMS,登录用户才能查看文章详情等等。
代码
parrot tag: auth-and-token
API 鉴权机制 JSON Web 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返回用户,此方法即为验证用户是否拥有api权限判断accesstoken的方法
- Authorizer.authorize,通过token判断此用户是否有某些permission的权限
- AuthFilter.newInstance,是通过java的建造者木事把上面几个类串到一起去的东西
- 登录判断用户密码正确后另外还需要写一个将user和token关联的方法 tokenize
Authenticator.authenticate 通过token的subject拿到用户唯一标识username
public Optional 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 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 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();
}
需要权限的API
之前写过的查看文章详情接口,不加@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.
总结
这个系列大概率完结了,因为剩下的东西在于具体的业务权限了,对于项目架构的东西基本完了。可能还需要做的是:
- 怎么让response的serializer更加项目式
- 怎么处理request参数的封装问题,难道一个接口要封装一个类么?
- mapper的通用父interface,例如封装常用的selectById,offset、limit等等是否有必要
上面的这些问题可能需要边写边重构,看是否有必要在写对应的文章好了。之后可能会去学一下react、react-router、react-redux做一下前端。