|
作者:小小____ 來源:segmentfault.com/a/1190000023052493 權(quán)限系統(tǒng)躲不開的概念,在Shiro和Spring Security之間,你一般選啥?在前后端分離的項(xiàng)目中,你知道怎么Spring security整合Jwt么,來看看這篇文章哈! RBAC 全稱為基于角色的權(quán)限控制,本段將會從什么是 RBAC,模型分類,什么是權(quán)限,用戶組的使用,實(shí)例分析等幾個(gè)方面闡述 RBAC。思維導(dǎo)圖

什么是 RBACRBAC 全稱為用戶角色權(quán)限控制,通過角色關(guān)聯(lián)用戶,角色關(guān)聯(lián)權(quán)限,這種方式,間階的賦予用戶的權(quán)限,如下圖所示

對于通常的系統(tǒng)而言,存在多個(gè)用戶具有相同的權(quán)限,在分配的時(shí)候,要為指定的用戶分配相關(guān)的權(quán)限,修改的時(shí)候也要依次的對這幾個(gè)用戶的權(quán)限進(jìn)行修改,有了角色這個(gè)權(quán)限,在修改權(quán)限的時(shí)候,只需要對角色進(jìn)行修改,就可以實(shí)現(xiàn)相關(guān)的權(quán)限的修改。這樣做增加了效率,減少了權(quán)限漏洞的發(fā)生。 模型分類對于 RBAC 模型來說,分為以下幾個(gè)模型 分別是 RBAC0,RBAC1,RBAC2,RBAC3,這四個(gè)模型,這段將會依次介紹這四個(gè)模型,其中最常用的模型有 RBAC0. RBAC0RBAC0 是最簡單的 RBAC 模型,這里面包含了兩種。 用戶和角色是多對一的關(guān)系,即一個(gè)用戶只充當(dāng)一種角色,一個(gè)角色可以有多個(gè)角色的擔(dān)當(dāng)。 用戶和角色是多對多的關(guān)系,即,一個(gè)用戶可以同時(shí)充當(dāng)多個(gè)角色,一個(gè)角色可以有多個(gè)用戶。 此系統(tǒng)功能單一,人員較少,這里舉個(gè)栗子,張三既是行政,也負(fù)責(zé)財(cái)務(wù),此時(shí)張三就有倆個(gè)權(quán)限,分別是行政權(quán)限,和財(cái)務(wù)權(quán)限兩個(gè)部分。 RBAC1相對于 RBAC0 模型來說,增加了子角色,引入了繼承的概念。

RBAC2 模型這里 RBAC2 模型,在 RBAC0 模型的基礎(chǔ)上,增加了一些功能,以及限制 角色互斥即,同一個(gè)用戶不能擁有兩個(gè)互斥的角色,舉個(gè)例子,在財(cái)務(wù)系統(tǒng)中,一個(gè)用戶不能擁有會計(jì)員和審計(jì)這兩種角色。 基數(shù)約束即,用一個(gè)角色,所擁有的成員是固定的,例如對于 CEO 這種角色,同一個(gè)角色,也只能有一個(gè)用戶。 先決條件即,對于該角色來說,如果想要獲得更高的角色,需要先獲取低一級別的角色。舉個(gè)栗子,對于副總經(jīng)理和經(jīng)理這兩個(gè)權(quán)限來說,需要先有副總經(jīng)理權(quán)限,才能擁有經(jīng)理權(quán)限,其中副總經(jīng)理權(quán)限是經(jīng)理權(quán)限的先決條件。 運(yùn)行時(shí)互斥即,一個(gè)用戶可以擁有兩個(gè)角色,但是這倆個(gè)角色不能同時(shí)使用,需要切換角色才能進(jìn)入另外一個(gè)角色。舉個(gè)栗子,對于總經(jīng)理和專員這兩個(gè)角色,系統(tǒng)只能在一段時(shí)間,擁有其一個(gè)角色,不能同時(shí)對這兩種角色進(jìn)行操作。 RBAC3 模型即,RBAC1,RBAC2,兩者模型全部累計(jì),稱為統(tǒng)一模型。
 什么是權(quán)限權(quán)限是資源的集合,這里的資源指的是軟件中的所有的內(nèi)容,即,對頁面的操作權(quán)限,對頁面的訪問權(quán)限,對數(shù)據(jù)的增刪查改的權(quán)限。舉個(gè)栗子。對于下圖中的系統(tǒng)而言,
 擁有,計(jì)劃管理,客戶管理,合同管理,出入庫通知單管理,糧食安全追溯,糧食統(tǒng)計(jì)查詢,設(shè)備管理這幾個(gè)頁面,對這幾個(gè)頁面的訪問,以及是否能夠訪問到菜單,都屬于權(quán)限。 用戶組的使用對于用戶組來說,是把眾多的用戶劃分為一組,進(jìn)行批量授予角色,即,批量授予權(quán)限。舉個(gè)栗子,對于部門來說,一個(gè)部門擁有一萬多個(gè)員工,這些員工都擁有相同的角色,如果沒有用戶組,可能需要一個(gè)個(gè)的授予相關(guān)的角色,在擁有了用戶組以后,只需要,把這些用戶全部劃分為一組,然后對該組設(shè)置授予角色,就等同于對這些用戶授予角色。 優(yōu)點(diǎn):減少工作量,便于理解,增加多級管理,等。 首先添加依賴 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
然后添加相關(guān)的訪問接口 package com.example.demo.web;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController @RequestMapping('/test') public class Test { @RequestMapping('/test') public String test(){ return 'test'; } } 關(guān)注公眾號:MarkerHub,回復(fù)[999]獲取前后端入門教程,以及企業(yè)面試題!
最后啟動項(xiàng)目,在日志中查看相關(guān)的密碼 
訪問接口,可以看到相關(guān)的登錄界面 
輸入用戶名和相關(guān)的密碼 用戶名:user 密碼 984cccf2-ba82-468e-a404-7d32123d0f9c

增加用戶名和密碼spring: security: user: name: ming password: 123456 roles: admin
基于內(nèi)存的認(rèn)證需要自定義類繼承 WebSecurityConfigurerAdapter 代碼如下package com.example.demo.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder(){ return NoOpPasswordEncoder.getInstance(); }
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser('admin').password('123').roles('admin'); } }
即,配置的用戶名為 admin,密碼為 123,角色為 adminHttpSecuritypackage 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 class SecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser('itguang').password('123456').roles('USER').and() .withUser('admin').password('{noop}' + '123456').roles('ADMIN'); }
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().permitAll() .and() .formLogin() .permitAll() .and() .logout() .permitAll(); }
}
這是一個(gè)小 demo,目的,登錄以后返回 jwt 生成的 token導(dǎo)入依賴 導(dǎo)入 JWT 和 Security 依賴 <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.3.1.RELEASE</version> </dependency>
創(chuàng)建一個(gè) JwtUser 實(shí)現(xiàn) UserDetails創(chuàng)建 一個(gè)相關(guān)的 JavaBeanpackage com.example.demo;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
public class JwtUser implements UserDetails { private String username; private String password; private Integer state; private Collection<? extends GrantedAuthority> authorities; public JwtUser(){
}
public JwtUser(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() { return this.password; }
@Override public String getUsername() { return this.username; }
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return true; } }
編寫工具類生成令牌編寫工具類,用來生成 token,以及刷新 token,以及驗(yàn)證 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 { private String secret; private Long expiration; private String header;
private String generateToken(Map<String, Object> claims) { Date expirationDate = new Date(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; }
public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(2); claims.put('sub', userDetails.getUsername()); claims.put('created', new Date()); return generateToken(claims);
}
public String getUsernameFromToken(String token) { String username; try { Claims claims = getClaimsFromToken(token); username = claims.getSubject();
} catch (Exception e) { username = null;
} return username;
}
public Boolean isTokenExpired(String token) { try { Claims claims = getClaimsFromToken(token); Date expiration = claims.getExpiration(); return expiration.before(new Date()); } catch (Exception e) { return false; } }
public String refreshToken(String token) { String refreshedToken; try { Claims claims = getClaimsFromToken(token); claims.put('created', new Date()); refreshedToken = generateToken(claims);
} catch (Exception e) { refreshedToken = null;
} return refreshedToken; }
public Boolean validateToken(String token, UserDetails userDetails) { JwtUser user = (JwtUser) userDetails; String username = getUsernameFromToken(token); return (username.equals(user.getUsername()) && !isTokenExpired(token));
}
}
編寫攔截器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 class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil;
@Override protected void doFilterInternal(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 的實(shí)現(xiàn)類在上方代碼中,編寫 userDetailsService,類,實(shí)現(xiàn)其驗(yàn)證過程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 class JwtUserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper;
@Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { User user = userMapper.selectByUserName(s); if (user == null) { throw new UsernameNotFoundException(String.format(''%s'.這個(gè)用戶不存在', s));
} List<SimpleGrantedAuthority> collect = user.getRoles().stream().map(Role::getRolename).map(SimpleGrantedAuthority::new).collect(Collectors.toList()); return new JwtUser(user.getUsername(), user.getPassword(), user.getState(), collect);
} }
編寫登錄編寫登錄業(yè)務(wù)的實(shí)現(xiàn)類 其 login 方法會返回一個(gè) JWTUtils 的 token@Service public class UserServiceImpl implements UserService { @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); return new RetResult(RetCode.SUCCESS.getCode(),jwtTokenUtil.generateToken(userDetails));
} }
最后配置 Config@EnableGlobalMethodSecurity(prePostEnabled = true) @EnableWebSecurity public class WebSecurity extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder.userDetailsService(this.userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder();
}
@Override protected void configure(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); return new CorsFilter(urlBasedCorsConfigurationSource);
} }
運(yùn)行,返回 token這里配置 SpringSecurity 之 JSON 登錄這里需要重寫 UsernamePasswordAnthenticationFilter 類,以及配置 SpringSecurity重寫 UsernamePasswordAnthenticationFilterpublic class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if(request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE) ||request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
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); return this.getAuthenticationManager().authenticate(authRequest); } }
else { return super.attemptAuthentication(request, response); } } }
配置 SecurityConfig@Override protected void configure(HttpSecurity http) throws Exception { http .cors().and() .antMatcher('/**').authorizeRequests() .antMatchers('/', '/login**').permitAll() .anyRequest().authenticated() .and().formLogin().loginPage('/') .and().csrf().disable();
http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); }
@Bean CustomAuthenticationFilter customAuthenticationFilter() throws Exception { CustomAuthenticationFilter filter = new CustomAuthenticationFilter(); filter.setAuthenticationSuccessHandler(new SuccessHandler()); filter.setAuthenticationFailureHandler(new FailureHandler()); filter.setFilterProcessesUrl('/login/self');
filter.setAuthenticationManager(authenticationManagerBean()); return filter; }
這樣就完成使用 json 登錄 SpringSecurity @Bean public BCryptPasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); }
即,使用此方法,對密碼進(jìn)行加密, 在業(yè)務(wù)層的時(shí)候,使用此加密的方法@Service @Transactional public class UserServiceImpl implements UserService {
@Resource private UserRepository userRepository;
@Resource private BCryptPasswordEncoder bCryptPasswordEncoder; @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 對密碼進(jìn)行加密,保存數(shù)據(jù)庫這里使用數(shù)據(jù)庫認(rèn)證 SpringSecurity設(shè)計(jì)數(shù)據(jù)表這里設(shè)計(jì)數(shù)據(jù)表
著重配置 SpringConfig@Configurable public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService;
@Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); }
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); }
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers('/admin/**').hasRole('admin') .anyRequest().authenticated() .and() .formLogin() .loginProcessingUrl('/login').permitAll() .and() .csrf().disable(); } }
著重講解了 RBAC 的權(quán)限配置,以及簡單的使用 SpringSecurity,以及使用 SpringSecurity + JWT 完成前后端的分離,以及配置 json 登錄,和密碼加密方式。
|