java RBAC @RolesAllowed实现原理和JWT

jakarta.annotation.security.*有个注解@RolesAllowed,专门用来做 RBAC (Role-base Access Control) 的。我们可以把它注解RESTful层的方法来限制访问,如POSTDELETEPUT方法的一些危险操作:

@Path("/user")
@Produces(MediaType.APPLICATION_JSON)
public class UserResource {
	@Inject
	private UserService srv;
	
	@POST
	@Consumes(MediaType.APPLICATION_JSON)
	@RolesAllowed({"User", "Admin"})
	public void add(User user) {
		srv.persist(user);
	}
}

UserResource#add方法注解后,每次请求这个方法都会检查用户传过来的JWT(JSON Web Token)加密令牌的groups字段是否包含User或者Adminrole

什么是JWT?

JWT是一种加密身份验证技术,是一串Base64URL编码,它大概长这样:请添加图片描述

JWT 的结构

JWT 以.分隔,分为三个字段,分别是:

  • header(头部)
  • payload(负载)
  • signature(签名)
<header>.<payload>.<signature>

头部结构

(解密后的)

{
	"typ": "JWT",
 	"alg": "RS256"
}

typ(type):类型
alg(algorithm):指示加密算法


负载结构

(解密后的)是最重要的字段,以键值对形式承载着用户数据

{
  "iss": "https://example.com/issuer",
  "iat": 1725505766,
  "exp": 1725506066,
  "jti": "325bcbfc-dd0f-4217-8cb8-ebf5090b0c5d",
  ... 还有一些非标准的字段
  "upn": "jdoe@quarkus.io", 这个是非标准字段
}

payload有以下字段
iss(issuer):签发者,一般为网站URL
sub(subject):主题,一般是UUID
aud(audience):接收者
nbf(notBefore):生效时间,令牌在此之前不可用
iat(issuedAt):签发时间
exp(expiresAt):过期时间
jti(jwtId):令牌的唯一标识符,一般用于防止重放攻击。
upn(userPrincipalName)非标准字段,Jakarta EE定义,用来传输用户信息(如用户名),可以用安全上下文
fn(@Context SecurityContext ctx) { ctx.getUserPrincipal().getName() }获取


签名结构

签名用于验证消息的发送方和消息的完整性。没啥需了解的。


回过来,@RolesAllowed会检查传过来的JWT的groups,如果不包含,返回Unauthorized

譬如:

@RequestScoped
@DeclareRoles({"Admin", "User", "Guest"})
@Path("/secured")
public class SecuredResource {

	@GET
	@RolesAllowed({"Admin", "User"}) // <-- 看这里
	@Produces(MediaType.TEXT_PLAIN)
	public String secured() {
		return "Authenticated!";
	}
}

如果我的请求的JWT payload为:

{
	...
	groups: ["Guest"]
}

请求这个接口就会报错 401 Unauthorized
如果为:

{
	...
	groups: ["User"]
}

就可以访问。

我用的是Quarkus框架的quarkus-smallrye-jwtquarkus-smallrye-jwt-build实现。这个实现指定的是JWT的groups字段,我不知道这是不是Jakarta EE规范中的一部分还是实现自定义。


怎么通过HTTP传递令牌?

包含在HTTP请求中:JWT令牌可以通过HTTP请求传递:

Authorization: Bearer <JWT>

自动添加请求头:我们可以在前端设置全局拦截器,这样子所有的请求都会自动发送

// JQuery
$.ajaxSetup({
  beforeSend: xhr => {
  	const token = localStorage.getItem('authToken')
  	if (token) {
  	  xhr.setRequestHeader('Authorization', `Bearer ${token}`)
  	}
  }
})

// Axios.js
axios.interceptors.request.use(config => {
  const token = localStorage.getItem('authToken');
  if (token) {
    config.headers.Authorization = 'Bearer ' + token;
  }
  return config;
}, err => Promise.reject(err));

如何向客户端发送并使其保存令牌?

向用户发送了JWT,怎么让客户端储存?

  • 保存在localStorage:不容易收到CSRF,但容易受到XSS攻击
  • 保存在sessionStorage:关闭浏览器就会清除,容易受到XSS攻击
  • 保存在cookies:可以设置SameSite为strict以防止CSRF,httpOnly为true防止XSS——最推荐
import jakarta.ws.rs.core.NewCookie;

import java.util.concurrent.TimeUnit;

public class Cookies {
	public static NewCookie withToken(String name, String token) {
		return new NewCookie.Builder(name).value(token)
				.expiry(TimeUnit.DAYS.toSeconds(7))
				.httpOnly(true)
				.setPath("/")
				.sameSite(NewCookie.SameSiteStrict)
				.build();
	}
}

@Path("/xxx")
@Produces(MediaType.APPLICATION_JSON)
public class XXXResource {
	public Response validate() {
		return Response.ok()
				.cookie(Cookies.withToken("jwt", Tokens.withRoles("User")))
				;
	}
}

生成一段带有Admin, User权限的JWT(Quarkus环境):

/src/main/resources/application.properties配置

mp.jwt.verify.publickey.location=publicKey.pem
mp.jwt.verify.issuer=https://example.com/issuer
smallrye.jwt.sign.key.location=privateKey.pem
import io.smallrye.jwt.build.Jwt;

@Test
public void echoJwt() {
	System.out.println(generateToken("User", "Admin"));
}

public static String generateTokem(String... roles) {
	return Jwt.issuer("https://example.com/issuer")
			.upn("jdoe@quarkus.io")
			.groups(List.of(roles))
			.sign();
}

然后就可以测试了

curl -X POST localhost:8080/user -H "Content-Type: application/json" -H "Authorization: Bearer <你的jwt>"

JWT 令牌用法

设计一个POST /login:一旦验证通过,如果没有JWT,就向客服端发送JWT保存在Cookies中;如果有,直接从Cookie中抽取。

import io.smallrye.jwt.build.Jwt;

public class JWT {
	public static String withRole(String... roles) {
		return Jwt.issuer("https://example.com/issuer")
                .upn("jdoe@quarkus.io")
                .groups(roles)
                .sign();
	}
}

@Path("/login")
public class LoginResource {
	private static final String COOKIE_JWT_KEY = "jwt";

	@Inject ...

	@POST
	@Consumes(MediaType.APPLICATION_JSON)
	public Response login(@Context SecurityContext ctx, 
	                      @CookieParam(COOKIE_JWT_KEY) String jwt,
	                      User user) {
		if (jwt == null) {
			return Response.noContent()
					.location("登入界面")
					.cookie(new NewCookie.Builder(COOKIE_JWT_KEY).value(JWT.withRole("User", "Admin")))
					.build();
		} else if (validate(jwt)) {
			return Response.noContent()
					location("登入界面");
					.build();
		} else {
			throw new ForbiddenException();
		}
		// 这样获取 JWT upn 声明
		ctx.getUserPrincipal() // -> java.security.Principal
			.getName(); // -> String
		
	}
}

如何获取

完整版看 https://quarkus.io/guides/security-jwt


Quarkus 集成

Quarkus CLI

quarkus extension add quarkus-smallrye-jwt
quarkus extension add quarkus-smallrye-build

/src/main/resources/application.properties

mp.jwt.verify.publickey.location=publicKey.pem
mp.jwt.verify.issuer=https://example.com/issuer

smallrye.jwt.sign.key.location=privateKey.pem

/src/main/resources/publicKey.pem

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

/src/main/resources/privateKey.pem

-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWK8UjyoHgPTLa
PLQJ8SoXLLjpHSjtLxMqmzHnFscqhTVVaDpCRCb6e3Ii/WniQTWw8RA7vf4djz4H
OzvlfBFNgvUGZHXDwnmGaNVaNzpHYFMEYBhE8VGGiveSkzqeLZI+Y02G6sQAfDtN
qqzM/l5QX8X34oQFaTBW1r49nftvCpITiwJvWyhkWtXP9RP8sXi1im5Vi3dhupOh
nelk5n0BfajUYIbfHA6ORzjHRbt7NtBl0L2J+0/FUdHyKs6KMlFGNw8O0Dq88qnM
uXoLJiewhg9332W3DFMeOveel+//cvDnRsCRtPgd4sXFPHh+UShkso7+DRsChXa6
oGGQD3GdAgMBAAECggEAAjfTSZwMHwvIXIDZB+yP+pemg4ryt84iMlbofclQV8hv
6TsI4UGwcbKxFOM5VSYxbNOisb80qasb929gixsyBjsQ8284bhPJR7r0q8h1C+jY
URA6S4pk8d/LmFakXwG9Tz6YPo3pJziuh48lzkFTk0xW2Dp4SLwtAptZY/+ZXyJ6
96QXDrZKSSM99Jh9s7a0ST66WoxSS0UC51ak+Keb0KJ1jz4bIJ2C3r4rYlSu4hHB
Y73GfkWORtQuyUDa9yDOem0/z0nr6pp+pBSXPLHADsqvZiIhxD/O0Xk5I6/zVHB3
zuoQqLERk0WvA8FXz2o8AYwcQRY2g30eX9kU4uDQAQKBgQDmf7KGImUGitsEPepF
KH5yLWYWqghHx6wfV+fdbBxoqn9WlwcQ7JbynIiVx8MX8/1lLCCe8v41ypu/eLtP
iY1ev2IKdrUStvYRSsFigRkuPHUo1ajsGHQd+ucTDf58mn7kRLW1JGMeGxo/t32B
m96Af6AiPWPEJuVfgGV0iwg+HQKBgQCmyPzL9M2rhYZn1AozRUguvlpmJHU2DpqS
34Q+7x2Ghf7MgBUhqE0t3FAOxEC7IYBwHmeYOvFR8ZkVRKNF4gbnF9RtLdz0DMEG
5qsMnvJUSQbNB1yVjUCnDAtElqiFRlQ/k0LgYkjKDY7LfciZl9uJRl0OSYeX/qG2
tRW09tOpgQKBgBSGkpM3RN/MRayfBtmZvYjVWh3yjkI2GbHA1jj1g6IebLB9SnfL
WbXJErCj1U+wvoPf5hfBc7m+jRgD3Eo86YXibQyZfY5pFIh9q7Ll5CQl5hj4zc4Y
b16sFR+xQ1Q9Pcd+BuBWmSz5JOE/qcF869dthgkGhnfVLt/OQzqZluZRAoGAXQ09
nT0TkmKIvlza5Af/YbTqEpq8mlBDhTYXPlWCD4+qvMWpBII1rSSBtftgcgca9XLB
MXmRMbqtQeRtg4u7dishZVh1MeP7vbHsNLppUQT9Ol6lFPsd2xUpJDc6BkFat62d
Xjr3iWNPC9E9nhPPdCNBv7reX7q81obpeXFMXgECgYEAmk2Qlus3OV0tfoNRqNpe
Mb0teduf2+h3xaI1XDIzPVtZF35ELY/RkAHlmWRT4PCdR0zXDidE67L6XdJyecSt
FdOUH8z5qUraVVebRFvJqf/oGsXc4+ex1ZKUTbY0wqY1y9E39yvB3MaTmZFuuqk8
f3cg+fr8aou7pr9SHhJlZCU=
-----END PRIVATE KEY-----

生成私钥和公钥

# 生成一个长度为 2048 的私钥,2048 是业内推荐长度
openssl genrsa -out privateKey.pem 2048

# 从私钥中提取公钥
openssl rsa -pubout -in privateKey.pem -out publicKey.pem
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
关于RBAC (Role-Based Access Control, 基于角色的访问控制) 的Java实现,它通常与数据库无关,但可以与MySQL一起使用来存储权限信息。以下是简要介绍: 1. **理解基本概念**[^2]:在Java中,实现RBAC的一个常见方式是使用ORM框架(如Hibernate)操作数据库。首先,你需要创建一个表来存储用户、角色和权限关联,例如: ```sql CREATE TABLE roles ( role_id INT PRIMARY KEY, role_name VARCHAR(50) NOT NULL ); CREATE TABLE permissions ( permission_id INT PRIMARY KEY, permission_name VARCHAR(50) NOT NULL ); CREATE TABLE role_permissions ( role_id INT, permission_id INT, FOREIGN KEY (role_id) REFERENCES roles(role_id), FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) ); ``` 这将允许你存储角色和它们拥有的权限。 2. **授权逻辑**[^1]:在Java应用中,你可以通过服务类或DAO(Data Access Object)来管理这些关系。例如,当用户登录时,你可以查询他们所属的角色,然后基于角色的权限决定他们能访问哪些资源: ```java // 假设User类有roleId字段 List<Role> roles = userRepository.getUserRoles(userId); for (Role role : roles) { Set<Permission> rolePermissions = permissionRepository.getPermissionsByRoleId(role.getId()); // 授权逻辑:检查用户是否有某个特定的权限 if (rolePermissions.contains(permission)) { allowAccess(); } } ``` 3. **权限验证**:在需要的地方(如REST API控制器或服务层),你可以使用这些角色权限信息来验证用户的请求: ```java @GetMapping("/protected-resource") public ResponseEntity<?> accessProtectedResource(@PathVariable Long roleId, @PathVariable Long permissionId) { Role currentRole = getRoleById(roleId); if (currentRole.hasPermission(permissionId)) { // 允许访问 return ResponseEntity.ok().build(); } else { // 拒绝访问 return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值