Spring Security實現RBAC權限管理
一、簡介
在企業應用中,認證和授權是非常重要的一部分內容,業界最出名的兩個框架就是大名鼎鼎的 Shiro和Spring Security。由於Spring Boot非常的流行,選擇Spring Security做認證和授權的 人越來越多,今天我們就來看看用Spring 和 Spring Security如何實現基於RBAC的權限管理。
二、基礎概念RBAC
RBAC是Role Based Access Control的縮寫,是基於角色的訪問控制。一般都是分為用戶(user), 角色(role),權限(permission)三個實體,角色(role)和權限(permission)是多對多的 關系,用戶(user)和角色(role)也是多對多的關系。用戶(user)和權限(permission) 之間沒有直接的關系,都是通過角色作為代理,才能獲取到用戶(user)擁有的權限。一般情況下, 使用5張表就夠了,3個實體表,2個關系表。具體的sql清參照項目示例。
三、集群部署
為了確保應用的高可用,一般都會將應用集群部署。但是,Spring Security的會話機制是基於session的, 做集群時對會話會產生影響。我們在這里使用Spring Session做分布式Session的管理。
四、技術選型
我們使用的技術框架如下:
- Spring Boot
- Spring Security
- Spring Data Redis
- Spring Session
- Mybatis-3.4.6
- Druid
- Thymeleaf(第一次使用)
五、具體實現
首先,我們需要完成整個框架的整合,使用Spring Boot非常的方便,配置application.properties文件即可, 配置如下:
#數據源配置
spring.datasource.username=你的數據庫用戶名
spring.datasource.password=你的數據庫密碼
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生成對應的實體和DAO,這里不贅述。
前面的這些都是准備工作,下面就要配置和使用Spring Security了,首先配置登錄的頁面和 密碼的規則,以及授權使用的技術實現等。我們創建MyWebSecurityConfig
繼承WebSecurityConfigurerAdapter
,並復寫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
,並在類上標明注解@EnableWebSecurity
,然后復寫configure
方法, 由於我們的授權是采用注解方式的,所以這里只寫了authorizeRequests()
,並沒有具體的授權信息。 接下來我們配置登錄url和登錄失敗的url,並沒有配置登錄成功的url,因為如果指定了登錄成功的url, 每次登錄成功后都會跳轉到這個url上。但是,我們大部分的業務場景都是登錄成功后,跳轉到登錄頁之前的 那個頁面,登錄頁之前的這個頁面是不定的。具體例子如下:
- 你在未登錄的情況下訪問了購物車頁,購物車頁需要登錄,跳轉到了登錄頁,登錄成功后你會返回購物車頁。
- 你又在未登錄的情況下訪問了訂單詳情頁,訂單詳情頁需要登錄,跳轉到了登錄頁,登錄后你會跳轉到訂單詳情頁。
所以,這里不需要指定登錄成功的url。
再來說說PasswordEncoder
這個Bean,Spring Security掃描到PasswordEncoder
這個Bean, 就會把它作為密碼的加密規則,這個我們使用NoOpPasswordEncoder
,沒有密碼加密規則,數據庫中 存的是密碼明文。如果需要其他加密規則可以參考PasswordEncoder
的實現類,也可以自己實現 PasswordEncoder
接口,完成自己的加密規則。
最后我們再類上標明注解@EnableGlobalMethodSecurity(prePostEnabled = true)
,這樣我們再 方法調用前會進行權限的驗證。
Spring Security提供的認證方式有很多種,比如:內存方式、LDAP方式。但是這些都和我們方式不符, 我們希望使用自己的用戶(User)來做認證,Spring Security也提供了這樣的接口,方便了我們的開發。 首先,需要實現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
方法都是需要你自己實現的,其中有一個方法大家需要注意一下,那就是 getAuthorities()
方法,它返回的是用戶具體的權限,在權限判定時,需要調用這個方法。 所以我們再User類中定義了一個權限集合的變量
@Getter@Setter private Set<SimpleGrantedAuthority> permissions;
其中SimpleGrantedAuthority
是Spring Security提供的一個簡單的權限實體,它的構造函數只有一個 權限編碼的字符串,大多數情況下,我們這個權限類就夠用了。
然后,我們實現Spring Security的UserDetailsService1
接口,完成用戶以及用戶權限的查詢, 代碼如下:
@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; } }
這樣,用戶在登錄時就會調用這個方法,完成用戶以及用戶權限的查詢。
到此,用戶認證過程就結束了,登錄成功后,會跳到首頁或者登錄頁的前一頁(因為沒有配置登錄成功的url), 登錄失敗會跳到登錄失敗的url。
我們再看看權限判定的過程,我們在MyWebSecurityConfig
類上標明了注解@EnableGlobalMethodSecurity(prePostEnabled = true)
,這使得我們 可以在方法上使用注解進行權限判定。我們在用戶登錄過程中查詢了用戶的權限,系統知道了用戶的權限,就可以進行權限的判定了。
我們看看方法上的權限注解,如下:
@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)
指明具體的權限,其中xxx可以使用SPel表達式。如果不想指明具體的權限,僅僅使用 登錄、任何人等權限的,可以如下:
- isAnonymous()
- isAuthenticated()
- isRememberMe()
還有其他的一些方法,請Spring Security官方文檔。
如果用戶不滿足指定的權限,會返回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的基本標簽,sec是Thymeleaf對Spring Security的擴展標簽,在頁面中我們進行權限的判定如下:
<div class="logout" sec:authorize="isAuthenticated()"> ............ </div>
只有用戶在登錄的情況下,才可以顯示這個div下的內容。
到此,Spring Security就給大家介紹完了,具體的項目代碼參照我的GitHub地址: https://github.com/liubo-tech/spring-security-rbac