08-搭建Rest服务 - 04权限管理

Using JWT RBAC

jwt和RBAC 相信大家都已经有理解,本章就直接看一下Quarkus框架下如何使用JWT 实现权限校验。

本章目标

  • 创建项目
  • 实现一个简单的角色校验
  • 如何生成和校验 Jwt Token
  • 自定义校验

1 创建项目

本章只以简单的Rest 服务来验证权限校验,不涉及数据库操作。

1.1 加入如下依赖:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-jwt-build</artifactId>
</dependency>

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>

1.2 一个简单的Rest接口

@Path("jwt")
@RequestScoped
public class JwtResource {


    @Inject
    JsonWebToken jwt;

    @GET
    @Path("unsecured")
    public String unsecured(@Context SecurityContext context){

        return getResponseString(context);
    }
    /**
     * @Description: 
     * @param @param 从JWT中获取返回值
     * @return @return 
     * @author TongRui乀
     * @throws
     * @date 2021/3/7 9:43 上午
     */
    private String getResponseString(SecurityContext ctx) {
        String name;
        if (ctx.getUserPrincipal() == null) {
            name = "anonymous";
        } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) {
            throw new InternalServerErrorException("Principal and JsonWebToken names do not match");
        } else {
            name = ctx.getUserPrincipal().getName();
        }
        return String.format("hello + %s,"
                        + " isHttps: %s,"
                        + " authScheme: %s,"
                        + " hasJWT: %s",
                name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt());
    }

    private boolean hasJwt() {
        return jwt.getClaimNames() != null;
    }
}
  • JsonWebToken 是Quarkus封装好的Principal Bean, 直接注入即可使用。
  • @RequestScoped 这里使用Request的作用域,Jwt的有效作用域,仅在当前请求可用。
  • @Context SecurityContext context 上下文中封装了本次请求的Principal信息。
  • getResponseString() 简单封装了权限信息 用于返回值。

1.3 测试一下在这里插入图片描述

这个接口我们没有添加任何的权限限制,所以获取的数据基本都是默认值。

2. 加入角色限制

2.1 加入权限限制

@GET
@Path("hasRole")
@RolesAllowed({"User", "Admin"})
public String hasRole(@Context SecurityContext context){
    return getResponseString(context);
}

注意 这里的方法还是在上面的Resource里写的。 使用@RolesAllowed 指定有权限的角色来访问这个接口。这里只允许User 和 Admin 这两个角色可以访问这个接口。
同时为了对比再加入一个不设置权限的接口。

@GET
@Path("permitAll")
@PermitAll
public String permitAll(@Context SecurityContext context){
    return getResponseString(context);
}
  • @PermitAll 允许所有人调用,无论是否认证。

上面的代码仅仅是在代码中指定了我的接口是否需要认证权限,但想要验证还需要两步。

  • 由于Quarkus的JWT认证默认的加密方式是RSA256,所以需要指定秘钥对的公钥用于解析Token。
  • 基于秘钥对私钥 生成认证需要的JWT token

2.2 配置公钥

token的校验Quarkus已经封装好,我们只需要配置校验需要的配置即可。这里我们只需要配置一下公钥地址。

mp.jwt.verify.publickey.location=META-INF/resources/publicKey.pem

所以我们需要在项目目录下创建这个文件。至于公私秘钥的生成,一般使用openssl 工具生成即可,多种方式都可以生成,大家自行选择。
在这里插入图片描述
如果嫌麻烦,这里贴一个官网的公钥:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq
Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR
TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e
UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9
AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn
sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x
nQIDAQAB
-----END PUBLIC KEY-----

2.3 生成Token

实际上生成Token的方式多种多样,只要是加密使用的私钥和我们配置的公钥是一个秘钥对就行了。
方式一:

public class GenerateTokenResource {

    public static void main(String[] args) {

        Jwt.issuer("https://example.com/issuer")
                .upn("jdoe@quarkus.io")
                .groups(new HashSet<>(Arrays.asList("User", "Admin")))
                .claim(Claims.birthdate.name(), "2021-03")
                .sign(); 
    }
}

在项目的resource包下创建这么一个类,位置无所谓。

  • Jwt 是jwt-build 提供的一个工具类,用来生成JWT token的。
  • 而 upn、groups等等都是Jwt封装的方法而已,本质还是设置 claim。
  • sign 方法 根据默认的 RS256算法计算签名。 无参方法需要在执行时需要指定smallrye.jwt.sign.key-location"
    属性来指定秘钥位置。 也可以在重载方法sign(String keyLocation)中指定秘钥位置。
mvn exec:java -Dexec.mainClass=org.acme.security.jwt.GenerateToken -Dexec.classpathScope=test -Dsmallrye.jwt.sign.key-location=privateKey.pem

或者

System.setProperty("smallrye.jwt.sign.key-location", "privateKey.pem");
var token = Jwt.issuer("https://example.com/issuer")
        .upn("jdoe@quarkus.io")
        .groups(new HashSet<>(Arrays.asList("User", "Admin")))
        .claim(Claims.birthdate.name(), "2021-03")
        .sign();

System.out.println(token);

直接执行方法即可

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsInVwbiI6Impkb2VAcXVhcmt1cy5pbyIsImdyb3VwcyI6WyJVc2VyIiwiQWRtaW4iXSwiYmlydGhkYXRlIjoiMjAyMS0wMyIsImlhdCI6MTYxNTYzOTczMiwiZXhwIjoxNjE1NjQwMDMyLCJqdGkiOiJhYjFjYWEzZi0yNzBhLTRkMTgtODJhMy1lNGUyNjdmMGY1OWYifQ.dSsSB7TRC4BVv3EF8-kF06hoOUEE11j8FN4YOigxKPF1zDJYnksmGOdIW2jEqk7wsKRYDgDuIGunZZfzyY8-Z7uQaBLA_92FdqT25LFbvk5-wTIwjC-q_IXQEQfxaV6Y-vmJir22QS4BtK9_grydoYTcDdItgHWbWeWHqoj4fbVxipnBYrRAt7Jz79h3y79Ag3Y7l8ZiF6QNv3OjggpCIBmxpkZRah9tZMC1Br0BBAapPTrJ-ImFfZqOzjYkLw3X8OmHb-1Qg0Omt3J2htcRApwkzkBTjJO5oKBR9lcISP_CJ_8yhR8qF2GassJRxXvva0oHXfiNIVT0c6Jp_wo5Tw

方式二:
使用第三方工具,jjwt

<dependency>
	<groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>
private String secret = "AyM1SysPpbyDfgZld3umj1qzKObwVMko";

    public static void main(String[] args) {

        var pub = FileUtil.readString(new File("/Users/gengmei/IdeaProjects/getting-start-quarkus/getting-started-jwt/src/test/resources/privateKey.pem"), "utf-8");

        HashMap<String, Object> claims = new HashMap<>();

        claims.put("upn", "jdoe@quarkus.io");
        claims.put("Birthday", "2021-03");
        claims.put("groups", new HashSet<>(Arrays.asList("User", "Admin")));
        claims.put("iss", "https://example.com/issuer");

        System.out.println(Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS256, Base64.getEncoder().encodeToString(pub.getBytes()))
                .compact());
    }
  • 上面说过Jwt.upn 或者groups都是对 clamis的封装 这里我们直接使用clamis就行。
  • 算法的话,这里如果使用RS256会报错,所以这里改用HS256。
  • 还有一个可能的坑是,我使用的是Java11,在这里执行的时候会使用低版本的api报错,这里可以加以下依赖解决。
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-impl</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-core</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
</dependency>

token 如下:

eyJhbGciOiJIUzI1NiJ9.eyJNUC1KV1Qgc3BlY2lmaWMgdW5pcXVlIHByaW5jaXBhbCBuYW1lIjoiamRvZUBxdWFya3VzLmlvIiwiQmlydGhkYXkiOiIyMDIxLTAzIiwiTVAtSldUIHNwZWNpZmljIGdyb3VwcyBwZXJtaXNzaW9uIGdyYW50IjpbIlVzZXIiLCJBZG1pbiJdfQ.Xd1tFwW6BRNpq0dlYfJiQxg-L37Fgvp9Yj-J_5qo9ow

2.4 验证

此时我们的hasRole接口需要的权限是,只允许有User和Admin角色的用户可以访问,之前我们生成的token是拥有这两个权限的。
在这里插入图片描述
测试是可以访问接口的。接下来重新生成一个token 用户Test权限。
在这里插入图片描述
结果是被拦住了。
在这里插入图片描述

3 JWTParser

JWTParser 是Quarkus提供的Token解析工具类,在我们的测试Demo中都是直接注入Token,这是因为Quarkus已经帮我们解析好了,但是由于它的生命周期是当前请求,当一些特殊情况下不能直接注入Token那我们可以通过JWTParser来解析Token。

import org.eclipse.microprofile.jwt.JsonWebToken;
import io.smallrye.jwt.auth.principal.JWTParser;
...
@Inject JWTParser parser;

String token = getTokenFromOidcServer();

// Parse and verify the token
JsonWebToken jwt = parser.parse(token);

4 自定义Token解析

上面一节说过,Quarkus会自动帮助我们解析Token,其实是通过io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipalFactory 这个工具来实现的。同时我们也可以自己实现。

@ApplicationScoped
@Alternative
@Priority(1)
public class TestJWTCallerPrincipalFactory extends JWTCallerPrincipalFactory {

    @Override
    public JWTCallerPrincipal parse(String token, JWTAuthContextInfo authContextInfo) throws ParseException {
        try {
            // Token has already been verified, parse the token claims only
            String json = new String(Base64.getUrlDecoder().decode(token.split("\\.")[1]), StandardCharsets.UTF_8);
            System.out.println(json);
            return new DefaultJWTCallerPrincipal(JwtClaims.parse(json));
        } catch (InvalidJwtException ex) {
            throw new ParseException(ex.getMessage());
        }
    }
}

5 总结

本章主要简单介绍了Quarkus中的权限管理,使用起来也非常简单,仅仅通过几个配置和几个注解即可完成。
对于如何整合到已有业务的权限系统后期可以考虑下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值