简介
上一节Springsecurity 用户认证
Springsecurity 拥有强大的认证和授权功能并且非常灵活,,一来说我们都i有以下需求
可以帮助应用程序实现以下两种常见的授权需求:
-
用户-权限-资源:例如张三的权限是添加用户、查看用户列表,李四的权限是查看用户列表
-
用户-角色-权限-资源:例如 张三是角色是管理员、李四的角色是普通用户,管理员能做所有操作,普通用户只能查看信息
RBAC权限模型
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型 即每个用户对应不同角色,每个角色对应不同功能
用户-角色-权限-资源
RBAC(Role-Based Access Control,基于角色的访问控制)是一种常用的数据库设计方案,它将用户的权限分配和管理与角色相关联。以下是一个基本的RBAC数据库设计方案的示例:
- 用户表(User table):包含用户的基本信息,例如用户名、密码和其他身份验证信息。
列名 | 数据类型 | 描述 |
---|---|---|
user_id | int | 用户ID |
username | varchar | 用户名 |
password | varchar | 密码 |
varchar | 电子邮件地址 | |
… | … | … |
- 角色表(Role table):存储所有可能的角色及其描述。
列名 | 数据类型 | 描述 |
---|---|---|
role_id | int | 角色ID |
role_name | varchar | 角色名称 |
description | varchar | 角色描述 |
… | … | … |
- 权限表(Permission table):定义系统中所有可能的权限。
列名 | 数据类型 | 描述 |
---|---|---|
permission_id | int | 权限ID |
permission_name | varchar | 权限名称 |
description | varchar | 权限描述 |
… | … | … |
- 用户角色关联表(User-Role table):将用户与角色关联起来。
列名 | 数据类型 | 描述 |
---|---|---|
user_role_id | int | 用户角色关联ID |
user_id | int | 用户ID |
role_id | int | 角色ID |
… | … | … |
- 角色权限关联表(Role-Permission table):将角色与权限关联起来。
列名 | 数据类型 | 描述 |
---|---|---|
role_permission_id | int | 角色权限关联ID |
role_id | int | 角色ID |
permission_id | int | 权限ID |
… | … | … |
在这个设计方案中,用户可以被分配一个或多个角色,而每个角色又可以具有一个或多个权限。通过对用户角色关联和角色权限关联表进行操作,可以实现灵活的权限管理和访问控制。
当用户尝试访问系统资源时,系统可以根据用户的角色和权限决定是否允许访问。这样的设计方案使得权限管理更加简单和可维护,因为只需调整角色和权限的分配即可,而不需要针对每个用户进行单独的设置。其实就是权限或者角色不会直接在用户属性里,而是在多表联查中赋予
比如
来获取用户权限或者角色
<select id="selectPermsByUserId" resultType="java.lang.String">
SELECT
DISTINCT m.`perms`
FROM
sys_user_role ur
LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
WHERE
user_id = #{userid}
AND r.`status` = 0
AND m.`status` = 0
</select>
基于request的授权
在首页的用户笔记中有项目目录,现在修改其中的俩个接口权限
官方架构
之前只要携带正确的token 就可以访问
修改配置文件后
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.addFilterBefore(new JwtAuthenticationTokenFilter(redisCache,jwtUtil), UsernamePasswordAuthenticationFilter.class)
.sessionManagement(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.POST,"/auth/login").permitAll() // 对登录接口允许匿名访问
.requestMatchers("/user/list").hasAuthority("user:list")
.requestMatchers("/user/add").hasAuthority("user:add")
.requestMatchers(HttpMethod.OPTIONS).permitAll()
// .requestMatchers("**").permitAll()
.anyRequest().authenticated())
.exceptionHandling(exception -> exception.authenticationEntryPoint(new MyAuthenticationEntryPoint()))
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
;
return http.build();
}
返回的内容是自己定义的json
但是响应码确实变成了403 security不允许访问
实现授权:权限-资源
实现不同权限访问不同接口
配置文件限定接口
@EnableMethodSecurity
@Configuration
@AllArgsConstructor
public class WebSecurityConfig {
private final ApplicationEventPublisher applicationEventPublisher;
@Autowired
private RedisCache redisCache;
@Autowired
private JwtUtil jwtUtil;
// 从配置文件注入 ioc先扫描配置文件
@Resource
UserMapper userMapper;
/**
* 这个Bean创建了一个认证管理器对象,它是Spring Security认证的核心组件之一。
* 认证管理器负责协调和管理认证流程,并委托给一个或多个认证提供者(在这里,使用了daoAuthenticationProvider)来进行具体的认证操作。
* 这里通过创建一个ProviderManager对象,将之前配置的daoAuthenticationProvider添加到认证管理器中。
* 还通过setAuthenticationEventPublisher()方法设置了一个事件发布器,用于在认证事件发生时发布相关的事件,
* 这里使用了DefaultAuthenticationEventPublisher,并传入了一个applicationEventPublisher对象,可能用于发布认证事件到Spring的事件机制中。
* @return
*/
@Bean
public AuthenticationManager authenticationManager() {
List<AuthenticationProvider> providerList = new ArrayList<>();
providerList.add(daoAuthenticationProvider());
ProviderManager providerManager = new ProviderManager(providerList);
providerManager.setAuthenticationEventPublisher(new DefaultAuthenticationEventPublisher(applicationEventPublisher));
return providerManager;
}
/**
* 是Spring Security用于处理基于数据库的用户认证的提供者。
* DaoAuthenticationProvider需要一个UserDetailsService对象来获取用户的详细信息进行认证,
* 所以通过setUserDetailsService()方法设置了我们之前设置的manager。
* @return
*/
@Bean
DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder( passwordEncoder());
daoAuthenticationProvider.setUserDetailsService(new DBUserDetailsManager(userMapper));
return daoAuthenticationProvider;
}
/**
* 把默认的密码加密器换成我们自定义的加密器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.addFilterBefore(new JwtAuthenticationTokenFilter(redisCache,jwtUtil), UsernamePasswordAuthenticationFilter.class)
.sessionManagement(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.POST,"/auth/login").permitAll() // 对登录接口允许匿名访问
.requestMatchers("/user/list").hasAuthority("user:list")//对接口定制限定权限
.requestMatchers("/user/add").hasAuthority("user:add")
.requestMatchers(HttpMethod.OPTIONS).permitAll()
// .requestMatchers("**").permitAll()
//对所有请求开启授权保护
.anyRequest()
//已认证的请求会被自动授权
.authenticated()
)
.exceptionHandling(
// 异常结果处理 1.认证异常处理2.授权异常处理
exception -> exception.authenticationEntryPoint(new MyAuthenticationEntryPoint())
.accessDeniedHandler((request, response, e)->{
//创建结果对象
HashMap result = new HashMap();
result.put("code", -1);
result.put("message", "没有权限");
//转换成json字符串
String json = JSON.toJSONString(result);
//返回响应
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
})
)
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
;
return http.build();
}
}
规范用户类
改造用户类
这里做简单模拟 写一个字段作为权限字段,往往实际开发中是一个json 数组用于转换
/**
* @TableName user
*/
@TableName(value ="user")
@Data
public class User implements Serializable {
private Integer id;
private String username;
private String password;
private Integer enabled;
//实际开发中权限字段多半是json 里卖装的权限数组
@TableField(exist = false)
private List<GrantedAuthority> authorities;
private static final long serialVersionUID = 1L;
}
security定义的规范用户类
@Data
public class UserDetail implements UserDetails {
private User user;
//存储SpringSecurity所需要的权限信息的集合 构造函数时候用于将user的json数组转换为这个权限对象
private List<GrantedAuthority> authorities;
@Override
public List<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
userdetailsService的实现类
@Component
@Slf4j
@AllArgsConstructor
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
private UserMapper userMapper;
// 这样就可以按照security的规范来使用用户的管理
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
return null;
}
@Override
public void createUser(UserDetails userDetails) {
// 在sql中插入信息
User user = new User();
user.setUsername(userDetails.getUsername());
user.setPassword(userDetails.getPassword());
user.setEnabled(1);
userMapper.insert(user);
}
@Override
public void updateUser(UserDetails user) {
}
@Override
public void deleteUser(String username) {
}
@Override
public void changePassword(String oldPassword, String newPassword) {
}
@Override
public boolean userExists(String username) {
return false;
}
//security底层会根据这个方法来对比用户
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 这里用户账户是唯一的
User user = userMapper.selectOne(Wrappers.<User>lambdaQuery().eq(User::getUsername, username));
if (user == null){
throw new UsernameNotFoundException("系统用户不存在");
}else{
// 1表示可用
boolean isenabled = user.getEnabled() == 1;
/**
* ,任何非零的整数值都会被视为 true,而 0 会被视为 false。
*/
log.info("数据库个根据用户名获取用户"+user);
//模拟系统权限列表
List<GrantedAuthority> authorities = new ArrayList<>();
// 写一个静态数据模拟用户数据库中的权限
authorities.add(()->"user:list");
user.setAuthorities(authorities);
UserDetail detail = new UserDetail();
detail.setUser(user);
detail.setAuthorities(authorities);
return detail;
}
}
}
改造过滤器
@Slf4j
@AllArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private final RedisCache redisCache;
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String token = request.getHeader("token");
if (tokenNotRequired(request.getRequestURI())) {
// 登录接口直接放行
filterChain.doFilter(request, response);
return;
}
if (token == null || token.trim().isEmpty()) {
throw new AuthenticationException("需要登录") {};
}
String username = jwtUtil.getUsernameFromToken(token);
String redisKey = "logintoken:" + username;
String jsonString = redisCache.getCacheObject(redisKey);
if (jsonString == null || jsonString.trim().isEmpty()) {
throw new AuthenticationException("用户登录已过期") {};
}
UserDetail userInfo = JSON.parseObject(jsonString, UserDetail.class);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userInfo, null, userInfo.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken); //设置给上下文对象
filterChain.doFilter(request, response);
}
//对所有抛出的异常进行处理
catch (AuthenticationException e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json; charset=utf-8");
response.getWriter().println(JSON.toJSONString(Result.nologin(e.getMessage())));
}
}
private boolean tokenNotRequired(String requestURI) {
return "/auth/login".equals(requestURI) || "/auth/info".equals(requestURI);
}
}
访问上下文对象
@GetMapping("/")
public Result index() {
SecurityContext context = SecurityContextHolder.getContext();//存储认证对象的上下文
Authentication authentication = context.getAuthentication();//认证对象
String username = authentication.getName();//用户名
Object principal =authentication.getPrincipal();//身份
Object credentials = authentication.getCredentials();//凭证(脱敏)
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();//权限
System.out.println(username);
System.out.println(principal);
System.out.println(credentials);
System.out.println(authorities);
HashMap<String, Object> map = new HashMap<>();
map.put("认证对象", authentication);
map.