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中的权限管理,使用起来也非常简单,仅仅通过几个配置和几个注解即可完成。
对于如何整合到已有业务的权限系统后期可以考虑下。