security jwt 动态权限控制RBAC0

RBAC0模型

最简单的用户、角色、权限模型。这里面又包含了2种:

用户和角色是多对一关系,即:一个用户只充当一种角色,一种角色可以有多个用户担当。
用户和角色是多对多关系,即:一个用户可同时充当多种角色,一种角色可以有多个用户担当。

那么,什么时候该使用多对一的权限体系,什么时候又该使用多对多的权限体系呢?

如果系统功能比较单一,使用人员较少,岗位权限相对清晰且确保不会出现兼岗的情况,此时可以考虑用多对一的权限体系。

其余情况尽量使用多对多的权限体系,保证系统的可扩展性。
如:张三既是行政,也负责财务工作,那张三就同时拥有行政和财务两个角色的权限。

项目环境

语言:kotlin(提供java版代码)
构建:gradle
框架:springboot + dubbo + jpa
按业务做的是1用户只能拥有1角色

FilterSecurityInterceptor

这个filter有几个要素,如下:

  • SecurityMetadataSource (动态获取url)
  • AccessDecisionManager (权限判断)
  • AuthenticationManager (jwt配置)

动态获取url权限配置

kotlin代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Component
class CustomMetadataSource : FilterInvocationSecurityMetadataSource {

@Reference // 用的dubbo,所以这里用这个注入
var systemResourceService: SystemResourceService? = null

private val antPathMatcher = AntPathMatcher()

override fun getAttributes(o: Any): Collection<ConfigAttribute> {
val fi = o as FilterInvocation
// 请求地址
val requestUrl = fi.requestUrl
// 获取系统权限资源配置 (这里就是去动态获取URL)
val resourceAndRole = systemResourceService!!.getResourceAndRole()

var needRoleList: MutableList<String> = ArrayList()

for (resourceRoleMap in resourceAndRole) {
// URL需要的角色,可能会有多个
if (antPathMatcher.match(resourceRoleMap["url"], requestUrl)) {
needRoleList.add(resourceRoleMap["role"].toString())
}
}

if (needRoleList.size > 0) {
val size = needRoleList.size
val values = arrayOfNulls<String>(size)
for (i in 0 until size) {
values[i] = needRoleList[i]
}
return SecurityConfig.createList(*values)
}

// 没有匹配上的资源,都是登录访问
return SecurityConfig.createList("ROLE_LOGIN")
}

override fun getAllConfigAttributes(): Collection<ConfigAttribute>? {
return null
}

override fun supports(clazz: Class<*>): Boolean {
return FilterInvocation::class.java.isAssignableFrom(clazz)
}

java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Component
public class CustomMetadataSource implements FilterInvocationSecurityMetadataSource {

@Autowired
MenuService menuService;

AntPathMatcher antPathMatcher = new AntPathMatcher();

@Override
public Collection<ConfigAttribute> getAttributes(Object o) {
String requestUrl = ((FilterInvocation) o).getRequestUrl();
List<Menu> allMenu = menuService.getAllMenu();
for (Menu menu : allMenu) {
if (antPathMatcher.match(menu.getUrl(), requestUrl) && menu.getRoles().size() > 0) {
List<Role> roles = menu.getRoles();
int size = roles.size();
String[] values = new String[size];
for (int i = 0; i < size; i++) {
values[i] = roles.get(i).getName();
}
return SecurityConfig.createList(values);
}
}
//没有匹配上的资源,都是登录访问
return SecurityConfig.createList("ROLE_LOGIN");
}

@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}

@Override
public boolean supports(Class<?> aClass) {
return FilterInvocation.class.isAssignableFrom(aClass);
}

权限资源URL所需角色判断

kotlin代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Component
class UrlAccessDecisionManager : AccessDecisionManager {

private val log = LoggerFactory.getLogger(this.javaClass)

override fun decide(authentication: Authentication, o: Any, cas: Collection<ConfigAttribute>) {
val ite = cas.iterator()
while (ite.hasNext()) {
val ca = ite.next()

// 当前请求需要的权限
val needRole = ca.attribute

// log.info("访问" + o.toString() + "需要角色:" + needRole)

if ("ROLE_LOGIN" == needRole) {
if (authentication is AnonymousAuthenticationToken) {
throw BadCredentialsException("未登录")
} else
throw AccessDeniedException("权限不足")
}

// 遍历判断该url所需的角色看用户是否具备
for (ga in authentication.authorities) {
if (ga.authority == needRole) {
// 匹配到有对应角色,则允许通过
return
}
}
}
throw AccessDeniedException("权限不足")
}

override fun supports(attribute: ConfigAttribute?): Boolean {
return true
}

override fun supports(clazz: Class<*>?): Boolean {
return true
}

java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Component
public class UrlAccessDecisionManager implements AccessDecisionManager {

@Override
public void decide(Authentication auth, Object o, Collection<ConfigAttribute> cas) {
Iterator<ConfigAttribute> iterator = cas.iterator();
while (iterator.hasNext()) {
ConfigAttribute ca = iterator.next();
// 当前请求需要的权限
String needRole = ca.getAttribute();
if ("ROLE_LOGIN".equals(needRole)) {
if (auth instanceof AnonymousAuthenticationToken) {
throw new BadCredentialsException("未登录");
} else {
throw new AccessDeniedException("权限不足");
}
}

// 当前用户所具有的权限
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("权限不足");
}

@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}

@Override
public boolean supports(Class<?> aClass) {
return true;
}
}

security配置

kotlin代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Configuration
@EnableWebSecurity
class WebSecurityConfig : WebSecurityConfigurerAdapter() {

@Autowired
var metadataSource: CustomMetadataSource? = null
@Autowired
var urlAccessDecisionManager: UrlAccessDecisionManager? = null
@Autowired
var deniedHandler: AuthenticationAccessDeniedHandler? = null

// 设置 HTTP 验证规则
override fun configure(http: HttpSecurity) {
// 关闭csrf验证
http.csrf().disable()
// 对请求进行认证
.authorizeRequests()
// 所有 / 的所有请求 都放行
.antMatchers("/").permitAll()
// 所有 /login 的POST请求 都放行
.antMatchers(HttpMethod.POST, "/login").permitAll()
// 所有请求需要身份认证
.anyRequest().authenticated()
// 动态配置URL权限
.withObjectPostProcessor(object : ObjectPostProcessor<FilterSecurityInterceptor> {
override fun <O : FilterSecurityInterceptor> postProcess(o: O): O {
o.securityMetadataSource = metadataSource
o.accessDecisionManager = urlAccessDecisionManager
return o
}
})
// 权限不足处理器
.exceptionHandling().accessDeniedHandler(deniedHandler)
}

java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
CustomMetadataSource metadataSource;
@Autowired
UrlAccessDecisionManager urlAccessDecisionManager;
@Autowired
AuthenticationAccessDeniedHandler deniedHandler;

@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
// 动态配置URL权限
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
public <O extends FilterSecurityInterceptor> O postProcess(
O fsi) {
fsi.setSecurityMetadataSource(metadataSource());
fsi.setAccessDecisionManager(urlAccessDecisionManager());
return fsi;
}
})
.and
// 权限不足处理器
.exceptionHandling().accessDeniedHandler(deniedHandler);
}
}

至此,Spring Security动态配置url权限就完成了

如果无需使用jwt,下面的可以不用添加


增加JWT认证功能

将用户id、拥有角色、过期时间放入token里。不建议持久化Token

因为服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展

添加Spring Security

kotlin完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@Configuration
@EnableWebSecurity
class WebSecurityConfig : WebSecurityConfigurerAdapter() {

@Autowired
var metadataSource: CustomMetadataSource? = null
@Autowired
var urlAccessDecisionManager: UrlAccessDecisionManager? = null
@Autowired
var deniedHandler: AuthenticationAccessDeniedHandler? = null

// 设置 HTTP 验证规则
override fun configure(http: HttpSecurity) {
// 关闭csrf验证
http.csrf().disable()
// 对请求进行认证
.authorizeRequests()
// 所有 / 的所有请求 都放行
.antMatchers("/").permitAll()
// 所有 /login 的POST请求 都放行
.antMatchers(HttpMethod.POST, "/login").permitAll()
// 所有请求需要身份认证
.anyRequest().authenticated()
// 动态配置URL权限
.withObjectPostProcessor(object : ObjectPostProcessor<FilterSecurityInterceptor> {
override fun <O : FilterSecurityInterceptor> postProcess(o: O): O {
o.securityMetadataSource = metadataSource
o.accessDecisionManager = urlAccessDecisionManager
return o
}
})
.and()
// 添加一个过滤器 所有访问 /login 的请求交给 JWTLoginFilter 来处理 这个类处理所有的JWT相关内容
.addFilterBefore(JWTLoginFilter("/login", authenticationManager()), UsernamePasswordAuthenticationFilter::class.java)
// 添加一个过滤器验证其他请求的Token是否合法
.addFilterBefore(JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter::class.java)
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler { req, response, authentication ->
response.status = HttpServletResponse.SC_OK
response.contentType = "application/json;charset=UTF-8"
val out = response.writer
val res = JSONResult(0, "注销成功", "")
out.write(ObjectMapper().writeValueAsString(res))
out.flush()
out.close()
}
.and()
// 权限不足处理器
.exceptionHandling().accessDeniedHandler(deniedHandler)
}

override fun configure(web: WebSecurity) {
web.ignoring().antMatchers(
"/index.html",
"/system/resource/open/**",
"/favicon.ico"
)
}

/**
* 这里需要SystemUserService实现UserDetailsService接口,重写loadUserByUsername方法,根据登录账号查询用户即可
* 用户实体类还需要继承UserDetails,重写方法
* 其中getAuthorities()方法见下方
*/
@Reference
var systemUserService: SystemUserService? = null

@Autowired
var bCryptPasswordEncoder: BCryptPasswordEncoder? = null

override fun configure(auth: AuthenticationManagerBuilder) {
// 盐加密
auth.userDetailsService(systemUserService).passwordEncoder(bCryptPasswordEncoder)
}

}

重写getAuthorities()方法:

1
2
3
4
5
6
7
8
override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
// 添加该用户拥有的角色
val authorities = ArrayList<GrantedAuthority>()
for (role in roles!!) {
authorities.add(SimpleGrantedAuthority(role.roleSign))
}
return authorities
}

java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Configuration
@EnableWebSecurity
class WebSecurityConfig extends WebSecurityConfigurerAdapter {

// 设置 HTTP 验证规则
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭csrf验证
http.csrf().disable()
// 对请求进行认证
.authorizeRequests()
// 所有 / 的所有请求 都放行
.antMatchers("/").permitAll()
// 所有 /login 的POST请求 都放行
.antMatchers(HttpMethod.POST, "/login").permitAll()
// 权限检查
.antMatchers("/hello").hasAuthority("AUTH_WRITE")
// 角色检查
.antMatchers("/world").hasRole("ADMIN")
// 所有请求需要身份认证
.anyRequest().authenticated()
.and()
// 添加一个过滤器 所有访问 /login 的请求交给 JWTLoginFilter 来处理 这个类处理所有的JWT相关内容
.addFilterBefore(new JWTLoginFilter("/login", authenticationManager()),
UsernamePasswordAuthenticationFilter.class)
// 添加一个过滤器验证其他请求的Token是否合法
.addFilterBefore(new JWTAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用自定义身份验证组件
auth.authenticationProvider(new CustomAuthenticationProvider());

}
}

Token生成和效验

参数读取的配置文件,可参考下方java代码

kotlin代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
@Component
object TokenAuthenticationService {

private val log = LoggerFactory.getLogger(javaClass)

@Value("\${jwt.expiration}")
fun setExpiration(expiration: Long?) {
EXPIRATION_TIME = expiration
}

@Value("\${jwt.secret}")
fun setSecret(secret: String?) {
SECRET = secret
}

@Value("\${jwt.header}")
fun setHeader(header: String?) {
HEADER_STRING = header
}

@Value("\${jwt.prefix}")
fun setPrefix(prefix: String?) {
TOKEN_PREFIX = prefix
}

var EXPIRATION_TIME: Long? = null // token有效期 1天
private var SECRET: String? = null // JWT密码
private var HEADER_STRING: String? = null // token前缀
private var TOKEN_PREFIX: String? = null // token前缀

/**
* JWT生成方法
*/
fun addAuthentication(response: HttpServletResponse, userId: String, authStr: String) {
// 生成JWT
val JWT = Jwts.builder()
// 保存权限(角色)
.claim("authorities", authStr)
// 用户ID写入标题
.setSubject(userId)
// 有效期设置
.setExpiration(Date(System.currentTimeMillis() + EXPIRATION_TIME!!))
// 签名设置
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact()

// 将 JWT 写入 body
try {
response.status = HttpServletResponse.SC_OK
response.contentType = "application/json;charset=UTF-8"
val out = response.writer
val res = JSONResult(0, "登录成功", JWT);
out.write(ObjectMapper().writeValueAsString(res))
out.flush()
out.close()
} catch (e: IOException) {
e.printStackTrace()
}
}

/**
* JWT验证方法
*/
fun getAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication? {
// 从Header中拿到token
val token = request.getHeader(HEADER_STRING)?:request.getParameter(HEADER_STRING)

if (!token.isNullOrBlank()) {
try {
// 解析 Token
val claims = Jwts.parser()
// 验签
.setSigningKey(SECRET)
// 去掉 Bearer
.parseClaimsJws(token.replace(TOKEN_PREFIX!!, "")).body

// 拿用户id
val user = claims.subject

// 得到 权限(角色)
val authorities = AuthorityUtils
.commaSeparatedStringToAuthorityList(claims["authorities"] as String)

// 返回验证令牌
return if (user != null) {
UsernamePasswordAuthenticationToken(user, null, authorities)
} else {
null
}
} catch (e: Exception) {
response.status = HttpServletResponse.SC_UNAUTHORIZED
response.contentType = "application/json;charset=UTF-8"
val out = response.writer

val res = JSONResult(5, "token失效", e.message)
out.write(ObjectMapper().writeValueAsString(res))
out.flush()
out.close()
}
}
return null
}

}

java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class TokenAuthenticationService {
static final long EXPIRATIONTIME = 432_000_000; // 5天
static final String SECRET = "P@ssw02d"; // JWT密码
static final String TOKEN_PREFIX = "Bearer"; // Token前缀
static final String HEADER_STRING = "Authorization";// 存放Token的Header Key

// JWT生成方法
static void addAuthentication(HttpServletResponse response, String username) {

// 生成JWT
String JWT = Jwts.builder()
// 保存权限(角色)
.claim("authorities", "ROLE_ADMIN,AUTH_WRITE")
// 用户名写入标题
.setSubject(username)
// 有效期设置
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
// 签名设置
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();

// 将 JWT 写入 body
try {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_OK);
response.getOutputStream().println(JSONResult.fillResultString(0, "", JWT));
} catch (IOException e) {
e.printStackTrace();
}
}

// JWT验证方法
static Authentication getAuthentication(HttpServletRequest request) {
// 从Header中拿到token
String token = request.getHeader(HEADER_STRING);

if (token != null) {
// 解析 Token
Claims claims = Jwts.parser()
// 验签
.setSigningKey(SECRET)
// 去掉 Bearer
.parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
.getBody();

// 拿用户名
String user = claims.getSubject();

// 得到 权限(角色)
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities"));

// 返回验证令牌
return user != null ?
new UsernamePasswordAuthenticationToken(user, null, authorities) :
null;
}
return null;
}
}

JWTLoginFilter 统一登录处理

创建一个类JWTLoginFilter,核心功能是在验证用户名密码正确后,生成一个token,并将token返回给客户端:

kotlin代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class JWTLoginFilter(url: String, authManager: AuthenticationManager) : AbstractAuthenticationProcessingFilter(AntPathRequestMatcher(url)) {

init {
authenticationManager = authManager
}

/**
* 拿到传入JSON,解析用户名密码
*/
override fun attemptAuthentication(req: HttpServletRequest, res: HttpServletResponse): Authentication {

// JSON反序列化成 AccountCredentials
val creds = ObjectMapper().readValue(req.inputStream, SystemUser::class.java)

// 返回一个验证令牌
return authenticationManager.authenticate(
UsernamePasswordAuthenticationToken(
creds.userAccount,
creds.pwd
)
)
}

// 登录成功
override fun successfulAuthentication(req: HttpServletRequest, res: HttpServletResponse, chain: FilterChain,
auth: Authentication) {
// 获取用户角色
val authorities = auth.authorities
var authStr: String? = null
for (granted in authorities) {
authStr += granted.authority + ","
}
// 生成令牌
val systemUser = auth.principal as SystemUser
TokenAuthenticationService.addAuthentication(res, systemUser.id!!, authStr!!.trimEnd(',').replace("null", "")!!)
}

// 登录失败
override fun unsuccessfulAuthentication(request: HttpServletRequest, response: HttpServletResponse,
failed: AuthenticationException) {
response.status = HttpServletResponse.SC_OK
response.contentType = "application/json;charset=UTF-8"
val out = response.writer

val res = JSONResult<String>(99, failed.message)

if (failed is BadCredentialsException) {
res.message = "账号或密码错误"
res.content = failed.toString()
}
else {
res.content = failed.toString()
}

out.write(ObjectMapper().writeValueAsString(res))
out.flush()
out.close()
}

java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;

public JWTLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}

@Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
try {
MyUser user = new ObjectMapper()
.readValue(req.getInputStream(), MyUser.class);

return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
user.getUsername(),
user.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException, ServletException {

String token = Jwts.builder()
.setSubject(((User) auth.getPrincipal()).getUsername())
.setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 24 * 1000))
.signWith(SignatureAlgorithm.HS512, "MyJwtSecret")
.compact();
res.addHeader("Authorization", "Bearer " + token);
}
}

JWTAuthenticationFilter 拦截所有请求做jwt效验

kotlin代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class JWTAuthenticationFilter : GenericFilterBean() {

override fun doFilter(request: ServletRequest, response: ServletResponse, filterChain: FilterChain) {
// 验证Token
val authentication = TokenAuthenticationService
.getAuthentication(request as HttpServletRequest, response as HttpServletResponse)

// 验证成功
if (authentication != null) {
SecurityContextHolder.getContext().authentication = authentication
filterChain.doFilter(request, response)
} else {
response.status = HttpServletResponse.SC_UNAUTHORIZED
response.contentType = "application/json;charset=UTF-8"
val out = response.writer
val res = JSONResult(6, "token验证失败", "")
out.write(ObjectMapper().writeValueAsString(res))
out.flush()
out.close()
}
}

}

java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class JWTAuthenticationFilter extends GenericFilterBean {

@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain filterChain)
throws IOException, ServletException {
Authentication authentication = TokenAuthenticationService
.getAuthentication((HttpServletRequest)request);

SecurityContextHolder.getContext()
.setAuthentication(authentication);
filterChain.doFilter(request,response);
}
}

一个完整的RBAC0体系就完成了,数据表和用户表按自己的业务来建就可以了
由于公司使用的是kotlin语言,java代码可能会不完全,差的不多,按逻辑修改下就行了
不推荐使用kotlin,没感觉到优势在哪里,还是java写着舒服

最后附上角色和资源权限的表结构,仅供参考

角色:

权限资源:

------本文结束感谢阅读------
0%