當前位置: 妍妍網 > 碼農

萬字長文,例項分析角色許可權控制

2024-03-20碼農

架構師(JiaGouX)

我們都是架構師!
架構未來,你來不來?

RBAC許可權分析

RBAC 全稱為基於角色的許可權控制,本文將會從什麽是RBAC,模型分類,什麽是許可權,使用者組的使用,例項分析等幾個方面闡述RBAC

思維導圖



什麽是RBAC

例項分析等幾個方面闡述RBAC

RBAC 全稱為使用者角色許可權控制,透過角色關聯使用者,角色關聯許可權,這種方式,間階的賦予使用者的許可權,如下圖所示

對於通常的系統而言,存在多個使用者具有相同的許可權,在分配的時候,要為指定的使用者分配相關的許可權,修改的時候也要依次的對這幾個使用者的許可權進行修改,有了角色這個許可權,在修改許可權的時候,只需要對角色進行修改,就可以實作相關的許可權的修改。這樣做增加了效率,減少了許可權漏洞的發生。

模型分類


對於RBAC模型來說,分為以下幾個模型 分別是RBAC0,RBAC1,RBAC2,RBAC3,這四個模型,這段將會依次介紹這四個模型,其中最常用的模型有RBAC0.

  • RBAC0

  • RBAC0是最簡單的RBAC模型,這裏麵包含了兩種。

  • 使用者和角色是多對一的關系 ,即一個使用者只充當一種角色,一個角色可以有多個角色的擔當。

  • 使用者和角色是多對多的關系 ,即,一個使用者可以同時充當多個角色,一個角色可以有多個使用者。


  • 此系統功能單一,人員較少,這裏舉個栗子,張三既是行政,也負責財務,此時張三就有倆個許可權,分別是行政許可權,和財務許可權兩個部份。

  • RBAC1

  • 相對於RBAC0模型來說,增加了子角色,引入了繼承的概念。

  • RBAC2 模型

  • 這裏RBAC2模型,在RBAC0模型的基礎上,增加了一些功能,以及限制

  • 角色互斥

    即,同一個使用者不能擁有兩個互斥的角色,舉個例子,在財務系統中,一個使用者不能擁有會計員和審計這兩種角色。

  • 基數約束

    即,用一個角色,所擁有的成員是固定的,例如對於CEO這種角色,同一個角色,也只能有一個使用者。

  • 先決條件

    即,對於該角色來說,如果想要獲得更高的角色,需要先獲取低一級別的角色。舉個栗子,對於副總經理和經理這兩個許可權來說,需要先有副總經理許可權,才能擁有經理許可權,其中副總經理許可權是經理許可權的先決條件。

  • 執行時互斥

    即,一個使用者可以擁有兩個角色,但是這倆個角色不能同時使用,需要切換角色才能進入另外一個角色。舉個栗子,對於總經理和專員這兩個角色,系統只能在一段時間,擁有其一個角色,不能同時對這兩種角色進行操作。

  • RBAC3模型

  • 即,RBAC1,RBAC2,兩者模型全部累計,稱為統一模型。

    什麽是許可權

    許可權是資源的集合,這裏的資源指的是軟體中的所有的內容,即,對頁面的操作許可權,對頁面的存取許可權,對數據的增刪查改的許可權。舉個栗子。對於下圖中的系統而言,


    擁有,計劃管理,客戶管理,合約管理,出入庫通知單管理,糧食安全追溯,糧食統計查詢,裝置管理這幾個頁面,對這幾個頁面的存取,以及是否能夠存取到選單,都屬於許可權。

    使用者組的使用

    對於使用者組來說,是把眾多的使用者劃分為一組,進行批次授予角色,即,批次授予許可權。 舉個栗子,對於部門來說,一個部門擁有一萬多個員工,這些員工都擁有相同的角色,如果沒有使用者組,可能需要一個個的授予相關的角色,在擁有了使用者組以後,只需要,把這些使用者全部劃分為一組,然後對該組設定授予角色,就等同於對這些使用者授予角色。

    優點:減少工作量,便於理解,增加多級管理,等。


    SpringSecurity 簡單使用



    首先添加依賴

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    然後添加相關的存取介面

    packagecom.example.demo.web;
    importorg.springframework.web.bind.annotation.RequestMapping;
    importorg.springframework.web.bind.annotation.RestController;
    @RestController
    @RequestMapping("/test")
    public class Test {
    @RequestMapping("/test")
    public String test(){
    return "test";
    }
    }

    最後啟動計畫,在日誌中檢視相關的密碼

    存取介面,可以看到相關的登入界面

    輸入使用者名稱和相關的密碼

    使用者名稱:user
    密碼 984cccf2-ba82-468e-a404-7d32123d0f9c

    登入成功

    增加使用者名稱和密碼


    在配置檔中,書寫相關的登入和密碼

    spring:
    security:
    user:
    name: ming
    password123456
    roles: admin

    在登入頁面,輸入使用者名稱和密碼,即可正常登入

    基於記憶體的認證


    需要自訂類繼承 WebSecurityConfigurerAdapter 程式碼如下

    packagecom.example.demo.config;
    importorg.springframework.context.annotation.Bean;
    importorg.springframework.context.annotation.Configuration;
    importorg.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    importorg.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    importorg.springframework.security.crypto.password.NoOpPasswordEncoder;
    importorg.springframework.security.crypto.password.PasswordEncoder;
    @Configuration
    public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    PasswordEncoder passwordEncoder(){
    returnNoOpPasswordEncoder.getInstance();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
    .withUser("admin").password("123").roles("admin");
    }
    }


    即,配置的使用者名稱為admin,密碼為123,角色為admin

    HttpSecurity


    這裏對一些方法進行攔截

    package com.ming.demo.interceptor;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.HttpMethod;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
    @Configuration
    @EnableWebSecurity
    public classSecurityConfigextendsWebSecurityConfigurerAdapter{
    //基於記憶體的使用者儲存
    @Override
    publicvoidconfigure(AuthenticationManagerBuilder auth)throws Exception {
    auth.inMemoryAuthentication()
    .withUser("itguang").password("123456").roles("USER").and()
    .withUser("admin").password("{noop}" + "123456").roles("ADMIN");
    }
    //請求攔截
    @Override
    protectedvoidconfigure(HttpSecurity http)throws Exception {
    http.authorizeRequests()
    .anyRequest().permitAll()
    .and()
    .formLogin()
    .permitAll()
    .and()
    .logout()
    .permitAll();
    }
    }



    即,這裏完成了對所有的方法存取的攔截。



    SpringSecurity 整合JWT



    這是一個小demo,目的,登入以後返回jwt生成的token

    匯入依賴

    添加web依賴

    匯入JWT和Security依賴

    <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
    <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.3.1.RELEASE</version>
    </dependency>

    建立一個JwtUser實作UserDetails

    建立 一個相關的JavaBean

    package com.example.demo;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    import java.util.Collection;
    public classJwtUserimplementsUserDetails{
    private String username;
    private String password;
    private Integer state;
    private Collection<? extends GrantedAuthority> authorities;
    publicJwtUser(){
    }
    publicJwtUser(String username, String password, Integer state, Collection<? extends GrantedAuthority> authorities){
    this.username = username;
    this.password = password;
    this.state = state;
    this.authorities = authorities;
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    return authorities;
    }
    @Override
    public String getPassword(){
    returnthis.password;
    }
    @Override
    public String getUsername(){
    returnthis.username;
    }
    @Override
    publicbooleanisAccountNonExpired(){
    returntrue;
    }
    @Override
    publicbooleanisAccountNonLocked(){
    returntrue;
    }
    @Override
    publicbooleanisCredentialsNonExpired(){
    returntrue;
    }
    @Override
    publicbooleanisEnabled(){
    returntrue;
    }
    }











    編寫工具類生成令牌

    編寫工具類,用來生成token,以及重新整理token,以及驗證token

    package com.example.demo;
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import org.springframework.security.core.userdetails.UserDetails;
    import java.io.Serializable;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    public class JwtTokenUtil implements Serializable {
    privateString secret;
    private Long expiration;
    privateString header;
    privateString generateToken(Map<StringObject> claims) {
    Date expirationDate = newDate(System.currentTimeMillis() + expiration);
    return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact();
    }
    private Claims getClaimsFromToken(String token) {
    Claims claims;
    try {
    claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    catch (Exception e) {
    claims = null;
    }
    return claims;
    }
    publicString generateToken(UserDetails userDetails) {
    Map<StringObject> claims = new HashMap<>(2);
    claims.put("sub", userDetails.getUsername());
    claims.put("created"newDate());
    return generateToken(claims);
    }
    publicString getUsernameFromToken(String token) {
    String username;
    try {
    Claims claims = getClaimsFromToken(token);
    username = claims.getSubject();
    catch (Exception e) {
    username = null;
    }
    return username;
    }
    publicBoolean isTokenExpired(String token) {
    try {
    Claims claims = getClaimsFromToken(token);
    Date expiration = claims.getExpiration();
    return expiration.before(newDate());
    catch (Exception e) {
    returnfalse;
    }
    }
    publicString refreshToken(String token) {
    String refreshedToken;
    try {
    Claims claims = getClaimsFromToken(token);
    claims.put("created"newDate());
    refreshedToken = generateToken(claims);
    catch (Exception e) {
    refreshedToken = null;
    }
    return refreshedToken;
    }
    publicBoolean validateToken(String token, UserDetails userDetails) {
    JwtUser user = (JwtUser) userDetails;
    String username = getUsernameFromToken(token);
    return (username.equals(user.getUsername()) && !isTokenExpired(token));
    }
    }


















    編寫攔截器

    編寫Filter 用來檢測JWT

    package com.example.demo;
    import org.apache.commons.lang.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
    import org.springframework.stereotype.Component;
    import org.springframework.web.filter.OncePerRequestFilter;
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    @Component
    public classJwtAuthenticationTokenFilterextendsOncePerRequestFilter{
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Override
    protectedvoiddoFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain)throws ServletException, IOException {
    String authHeader = httpServletRequest.getHeader(jwtTokenUtil.getHeader());
    if (authHeader != null && StringUtils.isNotEmpty(authHeader)) {
    String username = jwtTokenUtil.getUsernameFromToken(authHeader);
    if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
    UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
    if (jwtTokenUtil.validateToken(authHeader, userDetails)) {
    UsernamePasswordAuthenticationToken authentication =
    new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
    SecurityContextHolder.getContext().setAuthentication(authentication);
    }
    }
    }
    filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
    }





    編寫userDetailsService的實作類

    在上方程式碼中,編寫userDetailsService,類,實作其驗證過程

    package com.example.demo;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    import javax.management.relation.Role;
    import java.util.List;
    @Service
    public classJwtUserDetailsServiceImplimplementsUserDetailsService{
    @Autowired
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String s)throws UsernameNotFoundException {
    User user = userMapper.selectByUserName(s);
    if (user == null) {
    thrownew UsernameNotFoundException(String.format("'%s'.這個使用者不存在", s));
    }
    List<SimpleGrantedAuthority> collect = user.getRoles().stream().map(Role::getRolename).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    returnnew JwtUser(user.getUsername(), user.getPassword(), user.getState(), collect);
    }
    }





    編寫登入

    編寫登入業務的實作類 其login方法會返回一個JWTUtils 的token

    @Service
    public classUserServiceImplimplementsUserService{
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    public User findByUsername(String username){
    User user = userMapper.selectByUserName(username);
    return user;
    }
    public RetResult login(String username, String password)throws AuthenticationException {
    UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
    final Authentication authentication = authenticationManager.authenticate(upToken);
    SecurityContextHolder.getContext().setAuthentication(authentication);
    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
    returnnew RetResult(RetCode.SUCCESS.getCode(),jwtTokenUtil.generateToken(userDetails));
    }
    }






    最後配置Config


    @EnableGlobalMethodSecurity(prePostEnabled = true)
    @EnableWebSecurity
    public classWebSecurityextendsWebSecurityConfigurerAdapter{
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Autowired
    publicvoidconfigureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder)throws Exception {
    authenticationManagerBuilder.userDetailsService(this.userDetailsService).passwordEncoder(passwordEncoder());
    }
    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean()throws Exception {
    returnsuper.authenticationManagerBean();
    }
    @Bean
    public PasswordEncoder passwordEncoder(){
    returnnew BCryptPasswordEncoder();
    }
    @Override
    protectedvoidconfigure(HttpSecurity http)throws Exception {
    http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    .and().authorizeRequests()
    .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
    .antMatchers("/auth/**").permitAll()
    .anyRequest().authenticated()
    .and().headers().cacheControl();
    http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter. class);
    ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
    registry.requestMatchers(CorsUtils::isPreFlightRequest).permitAll();
    }
    @Bean
    public CorsFilter corsFilter(){
    final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
    final CorsConfiguration cors = new CorsConfiguration();
    cors.setAllowCredentials(true);
    cors.addAllowedOrigin("*");
    cors.addAllowedHeader("*");
    cors.addAllowedMethod("*");
    urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", cors);
    returnnew CorsFilter(urlBasedCorsConfigurationSource);
    }
    }












    執行,返回token

    執行,返回結果為token

    SpringSecurity JSON登入


    這裏配置SpringSecurity之JSON登入

    這裏需要重寫UsernamePasswordAnthenticationFilter類,以及配置SpringSecurity

    重寫UsernamePasswordAnthenticationFilter

    public classCustomAuthenticationFilterextendsUsernamePasswordAuthenticationFilter{
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)throws AuthenticationException {
    //attempt Authentication when Content-Type is json
    if(request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)
    ||request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
    //use jackson to deserialize json
    ObjectMapper mapper = new ObjectMapper();
    UsernamePasswordAuthenticationToken authRequest = null;
    try (InputStream is = request.getInputStream()){
    AuthenticationBean authenticationBean = mapper.readValue(is,AuthenticationBean. class);
    authRequest = new UsernamePasswordAuthenticationToken(
    authenticationBean.getUsername(), authenticationBean.getPassword());
    }catch (IOException e) {
    e.printStackTrace();
    authRequest = new UsernamePasswordAuthenticationToken(
    """");
    }finally {
    setDetails(request, authRequest);
    returnthis.getAuthenticationManager().authenticate(authRequest);
    }
    }
    //transmit it to UsernamePasswordAuthenticationFilter
    else {
    returnsuper.attemptAuthentication(request, response);
    }
    }
    }



    配置SecurityConfig

    @Override
    protectedvoidconfigure(HttpSecurity http)throws Exception {
    http
    .cors().and()
    .antMatcher("/**").authorizeRequests()
    .antMatchers("/""/login**").permitAll()
    .anyRequest().authenticated()
    //這裏必須要寫formLogin(),不然原有的UsernamePasswordAuthenticationFilter不會出現,也就無法配置我們重新的UsernamePasswordAuthenticationFilter
    .and().formLogin().loginPage("/")
    .and().csrf().disable();
    //用重寫的Filter替換掉原有的UsernamePasswordAuthenticationFilter
    http.addFilterAt(customAuthenticationFilter(),
    UsernamePasswordAuthenticationFilter. class);
    }
    //註冊自訂的UsernamePasswordAuthenticationFilter
    @Bean
    CustomAuthenticationFilter customAuthenticationFilter()throws Exception {
    CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
    filter.setAuthenticationSuccessHandler(new SuccessHandler());
    filter.setAuthenticationFailureHandler(new FailureHandler());
    filter.setFilterProcessesUrl("/login/self");
    //這句很關鍵,重用WebSecurityConfigurerAdapter配置的AuthenticationManager,不然要自己組裝AuthenticationManager
    filter.setAuthenticationManager(authenticationManagerBean());
    return filter;
    }


    這樣就完成使用json登入SpringSecurity

    Spring Security 密碼加密方式


    需要在Config 類中配置如下內容

    /**
    * 密碼加密
    */

    @Bean
    public BCryptPasswordEncoder passwordEncoder()
    {
    returnnew BCryptPasswordEncoder();
    }

    即,使用此方法,對密碼進行加密, 在業務層的時候,使用此加密的方法

    @Service
    @Transactional
    public classUserServiceImplimplementsUserService{
    @Resource
    private UserRepository userRepository;
    @Resource
    private BCryptPasswordEncoder bCryptPasswordEncoder; //註入bcryct加密
    @Override
    public User add(User user){
    user.setPassword(bCryptPasswordEncoder.encode(user.getPassword())); //對密碼進行加密
    User user2 = userRepository.save(user);
    return user2;
    }
    @Override
    public ResultInfo login(User user){
    ResultInfo resultInfo=new ResultInfo();
    User user2 = userRepository.findByName(user.getName());
    if (user2==null) {
    resultInfo.setCode("-1");
    resultInfo.setMessage("使用者名稱不存在");
    return resultInfo;
    }
    //判斷密碼是否正確
    if (!bCryptPasswordEncoder.matches(user.getPassword(),user2.getPassword())) {
    resultInfo.setCode("-1");
    resultInfo.setMessage("密碼不正確");
    return resultInfo;
    }
    resultInfo.setMessage("登入成功");
    return resultInfo;
    }
    }


    即,使用BCryptPasswordEncoder 對密碼進行加密,保存資料庫


    使用資料庫認證



    這裏使用資料庫認證SpringSecurity

    設計數據表

    這裏設計數據表

    著重配置SpringConfig

    @Configurable
    public classWebSecurityConfigextendsWebSecurityConfigurerAdapter{
    @Autowired
    private UserService userService; // service 層註入
    @Bean
    PasswordEncoder passwordEncoder(){
    returnnew BCryptPasswordEncoder();
    }
    @Override
    protectedvoidconfigure(AuthenticationManagerBuilder auth)throws Exception {
    // 參數傳入Service,進行驗證
    auth.userDetailsService(userService);
    }
    @Override
    protectedvoidconfigure(HttpSecurity http)throws Exception {
    http.authorizeRequests()
    .antMatchers("/admin/**").hasRole("admin")
    .anyRequest().authenticated()
    .and()
    .formLogin()
    .loginProcessingUrl("/login").permitAll()
    .and()
    .csrf().disable();
    }
    }


    這裏著重配置SpringConfig


    小結


    著重講解了RBAC的許可權配置,以及簡單的使用SpringSecurity,以及使用SpringSecurity + JWT 完成前後端的分離,以及配置json登入,和密碼加密方式。

    如喜歡本文,請點選右上角,把文章分享到朋友圈
    如有想了解學習的技術點,請留言給若飛安排分享

    因公眾號更改推播規則,請點「在看」並加「星標」 第一時間獲取精彩技術分享

    ·END·

    相關閱讀:

    作者:小小____

    來源:https://segmentfault.com/a/1190000023052493

    版權申明:內容來源網路,僅供學習研究,版權歸原創者所有。如有侵權煩請告知,我們會立即刪除並表示歉意。謝謝!

    架構師

    我們都是架構師!

    關註 架構師(JiaGouX),添加「星標」

    獲取每天技術幹貨,一起成為牛逼架構師

    技術群請 加若飛: 1321113940 進架構師群

    投稿、合作、版權等信箱: [email protected]