描述

上一篇(一个六年经验的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<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();
    }

需要权限的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做一下前端。


D咄咄
1.7k 声望257 粉丝

Life is to short, please use python.