一、用户授权
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。判断当前用户是否拥有访问当前资源所需的权限。SpringSecurity中的Authentication类如下所示:
public interface Authentication extends Principal, Serializable {//权限数据列表Collection<? extends GrantedAuthority> getAuthorities();Object getCredentials();Object getDetails();Object getPrincipal();boolean isAuthenticated();void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
前面登录时执行loadUserByUsername方法时,return new CustomUser(sysUser, Collections.emptyList());后面的空数据对接就是返回给Spring Security的权限数据。登录时我们把权限数据保存到redis中(用户名为key,权限数据为value即可),这样通过token获取用户名即可拿到权限数据,这样就可构成出完整的Authentication对象。
1、修改loadUserByUsername接口方法
package com.ywz.security.service;import com.ywz.pojo.SysUser;
import com.ywz.security.CustomUser;
import com.ywz.service.SysUserService;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.annotation.Resource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;/*** 类描述 -> 实现UserDetailsService接口,重写方法** @Author: ywz* @Date: 2024/07/28*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService{@Resourceprivate SysUserService sysUserService;@Resourceprivate SysMenuService sysMenuService; // 用于获取用户权限@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {SysUser sysUser = sysUserService.queryByUsername(username);if (Objects.isNull(sysUser)){throw new UsernameNotFoundException("用户名不存在!");}if(sysUser.getStatus() == 0) {throw new RuntimeException("账号已停用");}// 获取用户权限List<String> userPermsList = sysMenuService.findUserPermsList(sysUser.getId());List<SimpleGrantedAuthority> authorities = new ArrayList<>();for (String perm : userPermsList) {authorities.add(new SimpleGrantedAuthority(perm.trim()));}return new CustomUser(sysUser, authorities);}
}
2、修改配置类
配置类添加注解,开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认。
/*** 类描述 -> SpringSecurity配置类** @Author: ywz* @Date: 2024/07/28*/
@Configuration
@EnableWebSecurity // 是开启SpringSecurity的默认行为
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启SpringSecurity注解
public class SecurityConfig {/***内容和第一篇文章一致,所以省略****/
}
3、控制controller层接口权限
Spring Security默认是禁用注解的,要想开启注解,需要在继承WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,来判断用户对某个控制层的方法是否具有访问权限。通过@PreAuthorize标签控制controller层接口权限:
/*** 类描述 -> 角色前端控制器** @Author: ywz* @Date: 2024/07/28*/
public class SysRoleController {@Autowiredprivate SysRoleService sysRoleService;/*** 方法描述 -> 角色分页查询** @Return: @return {@link R }<{@link Page }* @Author: ywz* @Date: 2024/07/28*/@PreAuthorize("hasAuthority('policy:sysRole:list')") // 需要拥有该权限的用户才可以访问@ApiOperation(value = "获取分页列表")@GetMapping("/{page}/{limit}")public Result list(@ApiParam(name = "page", value = "当前页码", required = true)@PathVariable Long page,@ApiParam(name = "limit", value = "每页记录数", required = true)@PathVariable Long limit,@ApiParam(name = "roleQueryVo", value = "查询对象", required = false)SysRoleQueryVo roleQueryVo) {Page<SysRole> pageParam = new Page<>(page, limit);IPage<SysRole> pageModel = sysRoleService.selectPage(pageParam, roleQueryVo);return Result.ok(pageModel);}// ... 以下省略
}
4、测试服务器端权限
登录后台,分配权限进行测试,页面如果添加了按钮权限控制,可临时去除方便测试。测试结论:
- 分配了权限的能够成功返回接口数据
- 没有分配权限的会抛出异常:org.springframework.security.access.AccessDeniedException: 不允许访问
二、异常处理
如果希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
- 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
- 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。异常处理有2种方式:
- 扩展Spring Security异常处理类:AccessDeniedHandler、AuthenticationEntryPoint
- 在spring boot全局异常统一处理
注意:如果系统实现了全局异常处理,那么全局异常首先会获取AccessDeniedException异常,要想Spring Security扩展异常生效,必须在全局异常再次抛出该异常。
自定义异常实现类:
package com.ywz.security.filter;import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;/*** 类描述 -> 认证失败处理类** @Author: ywz* @Date: 2024/07/28*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {/*** 方法描述 -> 认证失败处理** @Return:* @Author: ywz* @Date: 2024/07/28*/@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {response.setStatus(200);// 自定义响应码枚举int code = ResultCodeEnum.LOGIN_AUTH.getCode();String msg = "认证失败,无法访问系统资源";response.setContentType("application/json;charset=UTF-8");Map<String, Object> result = new HashMap<>();result.put("msg", msg);result.put("code", code);String s = new ObjectMapper().writeValueAsString(result);response.getWriter().println(s);}
}
package com.ywz.security.filter;import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;/*** 类描述 -> 授权失败处理类** @Author: ywz* @Date: 2024/07/28*/
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {/*** 方法描述 -> 授权失败处理** @Author: ywz* @Date: 2024/07/28*/@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {int code = ResultCodeEnum.PERMISSION.getCode();response.setStatus(200);response.setContentType("application/json;charset=UTF-8");String msg = "权限不足,无法访问系统资源";Map<String, Object> result = new HashMap<>();result.put("msg", msg);result.put("code", code);String s = new ObjectMapper().writeValueAsString(result);response.getWriter().println(s);}
}
配置给SpringSecurity,在SecurityConfig中先注入对应的处理器:
@Resourceprivate AccessDeniedHandlerImpl accessDeniedHandler;@Resourceprivate AuthenticationEntryPointImpl authenticationEntryPoint;
然后在使用HttpSecurity对象的方法中添加相应配置。
return http// 基于 token,不需要 csrf.csrf().disable().exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler)// ... 以下省略