Spring Security實現(xiàn)RBAC權(quán)限管理
一簡介
在企業(yè)應(yīng)用中,認(rèn)證和授權(quán)是非常重要的一部分內(nèi)容,業(yè)界最出名的兩個框架就是大名鼎鼎的
Shiro和Spring Security窖铡。由于Spring Boot非常的流行,選擇Spring Security做認(rèn)證和授權(quán)的
人越來越多统捶,今天我們就來看看用Spring 和 Spring Security如何實現(xiàn)基于RBAC的權(quán)限管理七蜘。
二、基礎(chǔ)概念RBAC
RBAC是Role Based Access Control的縮寫捕仔,是基于角色的訪問控制匕积。一般都是分為用戶(user),
角色(role)榜跌,權(quán)限(permission)三個實體闪唆,角色(role)和權(quán)限(permission)是多對多的
關(guān)系,用戶(user)和角色(role)也是多對多的關(guān)系钓葫。用戶(user)和權(quán)限(permission)
之間沒有直接的關(guān)系悄蕾,都是通過角色作為代理,才能獲取到用戶(user)擁有的權(quán)限础浮。一般情況下帆调,
使用5張表就夠了,3個實體表豆同,2個關(guān)系表贷帮。具體的sql清參照項目示例。
三诱告、集群部署
為了確保應(yīng)用的高可用撵枢,一般都會將應(yīng)用集群部署民晒。但是,Spring Security的會話機(jī)制是基于session的锄禽,
做集群時對會話會產(chǎn)生影響潜必。我們在這里使用Spring Session做分布式Session的管理。
四沃但、技術(shù)選型
我們使用的技術(shù)框架如下:
- Spring Boot
- Spring Security
- Spring Data Redis
- Spring Session
- Mybatis-3.4.6
- Druid
- Thymeleaf(第一次使用)
五磁滚、具體實現(xiàn)
首先,我們需要完成整個框架的整合宵晚,使用Spring Boot非常的方便垂攘,配置application.properties文件即可,
配置如下:
#數(shù)據(jù)源配置
spring.datasource.username=你的數(shù)據(jù)庫用戶名
spring.datasource.password=你的數(shù)據(jù)庫密碼
spring.datasource.url=jdbc:mysql://localhost:3306/security_rbac?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
#mybatis配置
#mybatis.mapper-locations=mybatis/*.xml
#mybatis.type-aliases-package=com.example.springsecurityrbac.model
#redis配置
#spring.redis.cluster.nodes=149.28.37.147:7000,149.28.37.147:7001,149.28.37.147:7002,149.28.37.147:7003,149.28.37.147:7004,149.28.37.147:7005
spring.redis.host=你的redis地址
spring.redis.password=你的redis密碼
#spring-session配置
spring.session.store-type=redis
#thymeleaf配置
spring.thymeleaf.cache=false
然后淤刃,使用Mybatis Generator生成對應(yīng)的實體和DAO晒他,這里不贅述。
前面的這些都是準(zhǔn)備工作逸贾,下面就要配置和使用Spring Security了陨仅,首先配置登錄的頁面和
密碼的規(guī)則,以及授權(quán)使用的技術(shù)實現(xiàn)等铝侵。我們創(chuàng)建MyWebSecurityConfig
繼承WebSecurityConfigurerAdapter
灼伤,并復(fù)寫configure
方法,具體代碼如下:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.and()
.formLogin()
.loginPage("/login").failureForwardUrl("/login-error")
// .successForwardUrl("/index")
.permitAll();
}
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
}
我們繼承WebSecurityConfigurerAdapter
咪鲜,并在類上標(biāo)明注解@EnableWebSecurity
狐赡,然后復(fù)寫configure
方法,
由于我們的授權(quán)是采用注解方式的疟丙,所以這里只寫了authorizeRequests()
颖侄,并沒有具體的授權(quán)信息。
接下來我們配置登錄url和登錄失敗的url隆敢,并沒有配置登錄成功的url发皿,因為如果指定了登錄成功的url崔慧,
每次登錄成功后都會跳轉(zhuǎn)到這個url上拂蝎。但是,我們大部分的業(yè)務(wù)場景都是登錄成功后惶室,跳轉(zhuǎn)到登錄頁之前的
那個頁面温自,登錄頁之前的這個頁面是不定的。具體例子如下:
- 你在未登錄的情況下訪問了購物車頁皇钞,購物車頁需要登錄悼泌,跳轉(zhuǎn)到了登錄頁,登錄成功后你會返回購物車頁夹界。
- 你又在未登錄的情況下訪問了訂單詳情頁馆里,訂單詳情頁需要登錄,跳轉(zhuǎn)到了登錄頁,登錄后你會跳轉(zhuǎn)到訂單詳情頁鸠踪。
所以丙者,這里不需要指定登錄成功的url。
再來說說PasswordEncoder
這個Bean营密,Spring Security掃描到PasswordEncoder
這個Bean械媒,
就會把它作為密碼的加密規(guī)則,這個我們使用NoOpPasswordEncoder
评汰,沒有密碼加密規(guī)則纷捞,數(shù)據(jù)庫中
存的是密碼明文。如果需要其他加密規(guī)則可以參考PasswordEncoder
的實現(xiàn)類被去,也可以自己實現(xiàn)
PasswordEncoder
接口主儡,完成自己的加密規(guī)則。
最后我們再類上標(biāo)明注解@EnableGlobalMethodSecurity(prePostEnabled = true)
编振,這樣我們再
方法調(diào)用前會進(jìn)行權(quán)限的驗證缀辩。
Spring Security提供的認(rèn)證方式有很多種,比如:內(nèi)存方式踪央、LDAP方式臀玄。但是這些都和我們方式不符,
我們希望使用自己的用戶(User)來做認(rèn)證畅蹂,Spring Security也提供了這樣的接口健无,方便了我們的開發(fā)。
首先液斜,需要實現(xiàn)Spring Security的UserDetails
接口累贤,代碼如下:
public class User implements UserDetails {
@Generated("org.mybatis.generator.api.MyBatisGenerator")
private Integer id;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
private String username;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
private String password;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
private Boolean locked;
@Getter@Setter
private Set<SimpleGrantedAuthority> permissions;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public Integer getId() {
return id;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public void setId(Integer id) {
this.id = id;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return !locked;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public void setUsername(String username) {
this.username = username == null ? null : username.trim();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return permissions;
}
public void setAuthorities(Set<SimpleGrantedAuthority> permissions){
this.permissions = permissions;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public String getPassword() {
return password;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public void setPassword(String password) {
this.password = password == null ? null : password.trim();
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public Boolean getLocked() {
return locked;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public void setLocked(Boolean locked) {
this.locked = locked;
}
}
其中所有的@Override
方法都是需要你自己實現(xiàn)的,其中有一個方法大家需要注意一下少漆,那就是
getAuthorities()
方法臼膏,它返回的是用戶具體的權(quán)限,在權(quán)限判定時示损,需要調(diào)用這個方法渗磅。
所以我們再User類中定義了一個權(quán)限集合的變量
@Getter@Setter
private Set<SimpleGrantedAuthority> permissions;
其中SimpleGrantedAuthority
是Spring Security提供的一個簡單的權(quán)限實體,它的構(gòu)造函數(shù)只有一個
權(quán)限編碼的字符串检访,大多數(shù)情況下始鱼,我們這個權(quán)限類就夠用了。
然后脆贵,我們實現(xiàn)Spring Security的UserDetailsService1
接口医清,完成用戶以及用戶權(quán)限的查詢,
代碼如下:
@Service
public class SecurityUserService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private PermissionMapper permissionMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SelectStatementProvider selectStatement = select(UserDynamicSqlSupport.id,UserDynamicSqlSupport.username,UserDynamicSqlSupport.password,UserDynamicSqlSupport.locked)
.from(UserDynamicSqlSupport.user)
.where(UserDynamicSqlSupport.username,isEqualTo(username))
.build().render(RenderingStrategy.MYBATIS3);
Map<String,Object> parameter = new HashMap<>();
parameter.put("#{username}",username);
User user = userMapper.selectOne(selectStatement);
if (user == null) throw new UsernameNotFoundException(username);
SelectStatementProvider manyPermission = select(PermissionDynamicSqlSupport.id,PermissionDynamicSqlSupport.permissionCode,PermissionDynamicSqlSupport.permissionName)
.from(PermissionDynamicSqlSupport.permission)
.join(RolePermissionDynamicSqlSupport.rolePermission).on(RolePermissionDynamicSqlSupport.permissionId,equalTo(PermissionDynamicSqlSupport.id))
.join(UserRoleDynamicSqlSupport.userRole).on(UserRoleDynamicSqlSupport.roleId,equalTo(RolePermissionDynamicSqlSupport.roleId))
.where(UserRoleDynamicSqlSupport.userId,isEqualTo(user.getId()))
.build()
.render(RenderingStrategy.MYBATIS3);
List<Permission> permissions = permissionMapper.selectMany(manyPermission);
if (!CollectionUtils.isEmpty(permissions)){
Set<SimpleGrantedAuthority> sga = new HashSet<>();
permissions.forEach(p->{
sga.add(new SimpleGrantedAuthority(p.getPermissionCode()));
});
user.setAuthorities(sga);
}
return user;
}
}
這樣卖氨,用戶在登錄時就會調(diào)用這個方法会烙,完成用戶以及用戶權(quán)限的查詢负懦。
到此,用戶認(rèn)證過程就結(jié)束了柏腻,登錄成功后密似,會跳到首頁或者登錄頁的前一頁(因為沒有配置登錄成功的url),
登錄失敗會跳到登錄失敗的url葫盼。
我們再看看權(quán)限判定的過程残腌,我們在MyWebSecurityConfig
類上標(biāo)明了注解@EnableGlobalMethodSecurity(prePostEnabled = true)
,這使得我們
可以在方法上使用注解進(jìn)行權(quán)限判定贫导。我們在用戶登錄過程中查詢了用戶的權(quán)限抛猫,系統(tǒng)知道了用戶的權(quán)限,就可以進(jìn)行權(quán)限的判定了孩灯。
我們看看方法上的權(quán)限注解闺金,如下:
@PreAuthorize("hasAuthority(T(com.example.springsecurityrbac.config.PermissionContact).USER_VIEW)")
@RequestMapping("/user/index")
public String userIndex() {
return "user/index";
}
這是我們在Controller中的一段代碼,使用注解@PreAuthorize("hasAuthority(xxx)")
峰档,其中我們使用
hasAuthority(xxx)
指明具體的權(quán)限败匹,其中xxx可以使用SPel表達(dá)式。如果不想指明具體的權(quán)限讥巡,僅僅使用
登錄掀亩、任何人等權(quán)限的,可以如下:
- isAnonymous()
- isAuthenticated()
- isRememberMe()
還有其他的一些方法欢顷,請Spring Security官方文檔槽棍。
如果用戶不滿足指定的權(quán)限,會返回403錯誤信息抬驴。
由于前段我們使用的是Thymeleaf炼七,它對Spring Security的支持非常好,我們在pom.xml中添加如下配置:
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
<version>3.0.2.RELEASE</version>
</dependency>
并在頁面中添加如下引用:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
........
</html>
th是Thymeleaf的基本標(biāo)簽布持,sec是Thymeleaf對Spring Security的擴(kuò)展標(biāo)簽豌拙,在頁面中我們進(jìn)行權(quán)限的判定如下:
<div class="logout" sec:authorize="isAuthenticated()">
............
</div>
只有用戶在登錄的情況下,才可以顯示這個div下的內(nèi)容题暖。
到此按傅,Spring Security就給大家介紹完了,具體的項目代碼參照我的GitHub地址:
https://github.com/liubo-tech/spring-security-rbac