在企業應用中,認證和受權是很是重要的一部份內容,業界最出名的兩個框架就是大名鼎鼎的 Shiro和Spring Security。因爲Spring Boot很是的流行,選擇Spring Security作認證和受權的 人愈來愈多,今天咱們就來看看用Spring 和 Spring Security如何實現基於RBAC的權限管理。html
RBAC是Role Based Access Control的縮寫,是基於角色的訪問控制。通常都是分爲用戶(user), 角色(role),權限(permission)三個實體,角色(role)和權限(permission)是多對多的 關係,用戶(user)和角色(role)也是多對多的關係。用戶(user)和權限(permission) 之間沒有直接的關係,都是經過角色做爲代理,才能獲取到用戶(user)擁有的權限。通常狀況下, 使用5張表就夠了,3個實體表,2個關係表。具體的sql清參照項目示例。java
爲了確保應用的高可用,通常都會將應用集羣部署。可是,Spring Security的會話機制是基於session的, 作集羣時對會話會產生影響。咱們在這裏使用Spring Session作分佈式Session的管理。node
咱們使用的技術框架以下:mysql
首先,咱們須要完成整個框架的整合,使用Spring Boot很是的方便,配置application.properties文件便可, 配置以下:git
#數據源配置
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,這裏不贅述。github
前面的這些都是準備工做,下面就要配置和使用Spring Security了,首先配置登陸的頁面和 密碼的規則,以及受權使用的技術實現等。咱們建立MyWebSecurityConfig
繼承WebSecurityConfigurerAdapter
,並複寫configure
方法,具體代碼以下:redis
@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上。可是,咱們大部分的業務場景都是登陸成功後,跳轉到登陸頁以前的 那個頁面,登陸頁以前的這個頁面是不定的。具體例子以下:spring
因此,這裏不須要指定登陸成功的url。sql
再來講說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表達式。若是不想指明具體的權限,僅僅使用 登陸、任何人等權限的,能夠以下:
還有其餘的一些方法,請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