权限系统,在应用开发中是一种不可缺少的系统,主要进行权限的控制和管理,典型的权限设计方案有RBAC,常见的权限框架有shiro和security,下面对相关内容进行简单的聊聊。
一、RBAC
1、RBAC模型
RBAC模型(Role-Based Access Control:基于角色的访问控制),通过角色关联用户,角色关联权限,这种间接的方式赋予用户的权限,如下
2、RBAC组成
在RBAC模型中,有3个基础组成部分,分别是用户、角色和权限。RBAC通过定义角色的权限,并对用户授予某个角色从而控制用户的权限,实现了用户和权限的逻辑分离,方便了权限的管理。
- User(用户):每个用户都有唯一的UID识别,并被授予不同的角色;
- Role(角色):不同角色具有不同的权限;
- Permission(权限):访问权限;
- 用户-角色映射:用户和角色之间的映射关系;
- 角色-权限映射:角色和权限之间的映射;
如管理员和普通用户被授予不同的权限,普通用户只能去修改和查看用户,而不能创建创建用户和冻结用户,而管理员由于被授 予所有权限,所以可以做所有操作。
3、RBAC安全原则
RBAC支持三个著名的安全原则:最小权限原则、责任分离原则和数据抽象原则,如下
- 最小权限原则:RBAC可以将角色配置成其完成任务所需的最小权限集合;
- 责任分离原则:可以通过调用相互独立互斥的角色来共同完成敏感的任务,例如要求一个计账员和财务管理员共同参与统一过账操作;
- 数据抽象原则:可以通过权限的抽象来体现,例如财务操作用借款、存款等抽象权限,而不是使用典型的读、写、执行权限;
4、RBAC模型分类
RBAC模型可以分为以下4个模型:RBAC0、RBAC1、RBAC2、RBAC4,其中最简单常用的模型为RBAC0。
- RBAC0:最简单、最原始的实现方式,也是其他RBAC模型的基础;若用户和角色是多对一的关系,一个用户只充当一种角色,一个角色可以授予多个用户;若用户和角色是多对多的关系,一个用户可以同时充当多个角色,一个角色可以授予多个用户;
- RBAC1:基于RBAC0模型,引入了角色间的继承关系,即角色上有了上下级的区别;
- RBAC2:基于RBAC0模型的基础上,进行了角色的访问控制;一个基本限制是角色互斥,互斥角色是指各自权限可以互相制约的两个角色。对于这类角色一个用户在某一次活动中只能被分配其中的一个角色,不能同时获得两个角色的使用权;还有基数约束、先决条件、运行时互斥;
- RBAC3:RBAC1,RBAC2,两者模型全部累计,称为统一模型;
二、Spring Security
shiro和spring security都是安全框架,以spring security为例进行简单使用说明。
1、新建springboot工程,引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、访问接口
@GetMapping("/hello")
public String hello() {
return "hello";
}
3、启动访问
启动项目,如下
访问http://localhost:8080/hello,弹出如下
输入账号密码:user/控制台打印的字符串,返回如下
这样就完成了对访问的简单权限控制。
三、Spring Security + JWT实现前后端分离
实例如下
1、pom依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2、jwt工具类
@Component
public class JwtTokenUtil implements Serializable {
@Value("${jwt.header}")
private String header;
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
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 String getUserRole(String token) {
return (String) getClaimsFromToken(token).get("role");
}
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) {
String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public Long getExpiration() {
return expiration;
}
public void setExpiration(Long expiration) {
this.expiration = expiration;
}
public String getHeader() {
return header;
}
public void setHeader(String header) {
this.header = header;
}
}
3、jwt过滤器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Resource
private JwtUserDetailsServiceImpl userDetailsService;
@Resource
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain) throws servletexception, IOException {
String token = httpServletRequest.getHeader(jwtTokenUtil.getHeader());
if (token != null && StringUtils.hasLength(token)) {
String username = jwtTokenUtil.getUsernameFromToken(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validatetoken(token, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
4、重写userDetailsService的实现类
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {
@Resource
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
//直接写死数据信息,可以在这里获取数据库的信息并进行验证
// UserDetails user = User.withUsername("caocao")
// .password(passwordEncoder.encode("123"))
// .authorities("admin")
// .build();
// 从数据库根据name查询获得
User user = new User("caocao", passwordEncoder.encode("123"),
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
if (user == null) {
throw new UsernameNotFoundException(String.format("'%s'.这个用户不存在", name));
}
return user;
}
}
5、配置类
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private JwtUserDetailsServiceImpl userDetailsService;
@Resource
private JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Resource
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Resource
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
// 禁用session机制
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests()
// 指定某些接口不需要通过验证即可访问。像登陆、注册接口肯定是不需要认证的
.antMatchers("/login").permitAll()
.anyRequest().authenticated();
// 禁用缓存
http.headers().cacheControl();
// 使用自己定义的拦截机制验证请求是否正确,拦截jwt
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
// 添加自定义未授权和未登陆结果返回
// 用户访问没有授权资源
http.exceptionHandling().accessDeniedHandler(jwtAccessDeniedHandler);
// 用户访问资源没有携带正确的token
http.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint);
}
}
6、登录接口
@PostMapping("/login")
public String login(@RequestBody Map<String,String> params) {
//生成token,返回给客户端
UserDetails userDetails = userDetailsService.loadUserByUsername(params.get("userName"));
if (!passwordEncoder.matches(params.get("password"),userDetails.getpassword())) {
return "用户名或密码不正确";
}
String token = jwtTokenUtil.generatetoken(userDetails);
return token;
}
7、测试
直接访问hello接口,由于未登录返回
输入用户名/密码,密码错误时返回
输入正确时返回
携带jwt令牌访问hello接口,返回
以上只是其中一种方式的简单使用,工程化时还需深入理解security的底层原理及各种实现方式,若有错误或不当之处,欢迎指正。