一、引言
1、什么是SpringSecurity授权
Spring Security授权是指基于Spring Security框架的访问控制过程。授权是指根据系统提前设置好的规则,给用户分配可以访问某一个资源的权限,用户根据自己所具有的权限,去执行相应操作。在Spring Security中,授权通常与身份验证一起使用,以确保只有经过身份验证的用户才能访问特定的资源。
Spring Security授权的核心概念包括:
- 认证:身份认证,即判断一个用户是否为合法用户的处理过程。Spring Security支持多种不同方式的认证,但无论使用哪种方式,都不会影响授权功能的使用。因为Spring Security很好地做到了认证和授权的解耦。
- 授权:访问控制,即控制谁能访问哪些资源。简单的理解就是根据系统提前设置好的规则,给用户分配可以访问某一个资源的权限,用户根据自己所具有的权限,去执行相应操作。
在Spring Security中,授权通常通过安全配置类来实现。这个类定义了哪些URL需要授权以及如何处理安全相关的异常。它使用过滤器链来拦截请求并检查用户的权限。如果用户通过了身份验证并且具有访问特定资源的权限,他们就可以成功访问该资源。
2、授权介绍
Spring Security 中的授权分为两种类型:
- 基于角色的授权:以用户所属角色为基础进行授权,如管理员、普通用户等,通过为用户分配角色来控制其对资源的访问权限。
- 基于资源的授权:以资源为基础进行授权,如 URL、方法等,通过定义资源所需的权限,来控制对该资源的访问权限。
Spring Security 提供了多种实现授权的机制,最常用的是使用基于注解的方式,建立起访问资源和权限之间的映射关系。
其中最常用的两个注解是 @Secured
和 @PreAuthorize
。@Secured
注解是更早的注解,基于角色的授权比较适用,@PreAuthorize
基于 SpEL
表达式的方式,可灵活定义所需的权限,通常用于基于资源的授权。
二、SpringSecurity授权
根据 【Spring Security】认证之案例的使用、MD5加密、CSRF防御中表设计进行加强
1、修改User配置角色和权限
定义SQL
语句,根据用户ID查询角色和角色对应的权限。
- 根据用户ID查询出用户对应的角色信息。
SELECT
r.rolename
FROM
sys_user u,sys_user_role ur,sys_role r
where
u.id=ur.userid and ur.roleid=r.roleid and u.id=#{userid}
- 根据用户ID查询出角色对应的权限信息。
select
m.url
from
sys_user u,sys_user_role ur,sys_role r,sys_role_module rm,sys_module m
where
u.id=ur.userid and ur.roleid=r.roleid and
r.roleid=rm.roleid and rm.moduleid=m.id and
u.id=#{userid} and url is not null
修改User实体类,添加角色和权限集合,并完成角色和权限的数据填充。
@Getter
@Setter
@Accessors(chain = true)
@TableName("sys_user")
public class User implements Serializable, UserDetails {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 用户名
*/
@TableField("username")
private String username;
/**
* 密码
*/
@TableField("password")
private String password;
/**
* 真实姓名
*/
@TableField("real_name")
private String realName;
/**
* 是否过期
*/
@TableField("account_non_expired")
private boolean accountNonExpired;
/**
* 权限
*/
@TableField(exist = false)
private List? extends GrantedAuthority> authorities;
/**
* 是否锁定
*/
@TableField("account_non_locked")
private boolean accountNonLocked;
/**
* 是否过期
*/
@TableField("credentials_non_expired")
private boolean credentialsNonExpired;
/**
* 是否启用
*/
@TableField("enabled")
private boolean enabled;
}
2、修改loadUserByUsername方法
@Service
public class UserServiceImpl extends ServiceImplUserMapper, User>
implements UserService,UserDetailsService {
@Autowired
private RoleMapper roleMapper;
@Autowired
private RoleModuleMapper roleModuleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询数据库中用户信息
User user = this.getOne(new QueryWrapperUser>().eq("username", username));
//判断用户是否存在
if(Objects.isNull(user))
throw new UsernameNotFoundException("用户不存在");
//权限校验TODO,后续讲解
ListString> roles = roleMapper.queryRolesByUid(user.getId());
ListString> permission = roleModuleMapper.queryRoleModuleByUid(user.getId());
user.setRoles(roles);
user.setPermissions(permission);
return user;
}
}
3、修改SpringSecurity配置类
当我们想要开启spring
方法级安全时,只需要在任何 @Configuration
实例上使用@EnableGlobalMethodSecurity
注解就能达到此目的。同时这个注解为我们提供了prePostEnabled
、securedEnabled
和 jsr250Enabled
三种不同的机制来实现同一种功能。
修改WebSecurityConfig
配置类,开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {
//@Autowired 注解用于标注在需要进行依赖注入的属性或方法上
@Autowired
private MyUserDetailsService userDetailsService;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Bean//@Bean 注解用于标注在需要进行依赖注入的属性或方法上
public PasswordEncoder passwordEncoder() {
//BCryptPasswordEncoder 用于对密码进行编码
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager() throws Exception {
// 创建一个DaoAuthenticationProvider对象
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
// 设置用户详情服务和密码编码器
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
// 返回一个ProviderManager对象
return new ProviderManager(provider);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 允许所有用户访问根路径
.antMatchers("/").permitAll()
// 允许管理员访问管理员路径
.antMatchers("/admin/**").hasRole("ADMIN")
// 允许管理员和用户访问用户路径
.antMatchers("/user/**").hasAnyRole("ADMIN", "USER")
// 需要身份认证 anyRequest 其余所有请求 & authenticated 登录
.anyRequest().authenticated()
.and()
.formLogin()
//loginPage 登录页面
.loginPage("/")
//设置处理登录请求的接口
.loginProcessingUrl("/userLogin")
//用户的数据的参数
.usernameParameter("username")
.passwordParameter("password")
//登录成功
.successHandler((req, resp, auth) -> {
//获取用户信息
Object user = auth.getPrincipal();
//返回json格式的用户信息
objectMapper
.writeValue(resp.getOutputStream(), JsonResponseBody.success(user));
})
//登录失败
.failureHandler(myAuthenticationFailureHandler)
.and()
.exceptionHandling()
//权限不足
.accessDeniedHandler((req, resp, ex) -> {
//返回json格式的无权限信息
objectMapper
.writeValue(resp.getOutputStream(), JsonResponseBody.other(JsonResponseStatus.NO_ACCESS));
})
//没有认证
.authenticationEntryPoint((req, resp, ex) -> {
//返回json格式的未登录信息
objectMapper
.writeValue(resp.getOutputStream(), JsonResponseBody.other(JsonResponseStatus.NO_LOGIN));
})
.and()
.logout()
//设置登出url
.logoutUrl("/logout")
//设置登出成功url
.logoutSuccessUrl("/");
//禁用csrf
http.csrf().disable();
//返回http实例
return http.build();
}
}
@EnableGlobalMethodSecurity
是Spring Security提供的一个注解,用于启用方法级别的安全性。它可以在任何@Configuration类上使用,以启用Spring Security的方法级别的安全性功能。它接受一个或多个参数,用于指定要使用的安全注解类型和其他选项。以下是一些常用的参数:
-
prePostEnabled
:如果设置为true
,则启用@PreAuthorize
和@PostAuthorize
注解。默认值为false
。 -
securedEnabled
:如果设置为true
,则启用@Secured
注解。默认值为false
。 -
jsr250Enabled
:如果设置为true
,则启用@RolesAllowed
注解。默认值为false
。 -
proxyTargetClass
:如果设置为true
,则使用CGLIB代理而不是标准的JDK动态代理。默认值为false
。
使用@EnableGlobalMethodSecurity
注解后,可以在应用程序中使用Spring Security提供的各种注解来保护方法,例如@Secured
、@PreAuthorize
、@PostAuthorize
和@RolesAllowed
。这些注解允许您在方法级别上定义安全规则,以控制哪些用户可以访问哪些方法。
注解介绍:
注解 | 说明 |
---|---|
@PreAuthorize |
用于在方法执行之前对访问进行权限验证 |
@PostAuthorize |
用于在方法执行之后对返回结果进行权限验证 |
@Secured |
用于在方法执行之前对访问进行权限验证 |
@RolesAllowed |
是Java标准的注解之一,用于在方法执行之前对访问进行权限验证 |
4、控制Controller层接口权限
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private RoleService roleService;
@Autowired
private ModuleService moduleService;
@RequestMapping("/userLogin")
public String userLogin(User user){
return "login";
}
@PreAuthorize("hasAuthority('order:manager:list')")
@GetMapping("/queryRoles")
public JsonResponseBodyListRole>> queryRoles(){
ListRole> list = roleService.list();
return new JsonResponseBody>(list);
}
@PreAuthorize("hasAuthority('book:manager:list')")
@GetMapping("/queryModules")
public JsonResponseBodyListModule>> queryModules(){
ListModule> list = moduleService.list();
return new JsonResponseBody>(list);
}
@PreAuthorize("hasAuthority('管理员')")
@GetMapping("/queryTest")
public JsonResponseBody?> queryTest(){
return new JsonResponseBody>("你好,我是管理员!");
}
}
更多常见内置表达式见官方文档
5、启动测试
配置完毕之后,重新启动项目。分别使用两个不同的用户(admin
和zs
)登录进行权限测试。
注意:
admin
具备所有权限;zs
只具备部分权限。
当通过zs
用户登录成功之后,点击权限和角色验证进行权限测试。
-
点击获取用户角色信息,可以成功显示数据:
-
点击获取角色权限信息,提示403错误:(也就是无权限提示)
6、异常处理
①AccessDeniedHandler
AccessDeniedHandler
是Spring Security提供的一个接口,用于处理访问被拒绝的情况。当用户尝试访问受保护资源但没有足够的权限时,Spring Security会调用AccessDeniedHandler
来处理这种情况。
AccessDeniedHandler
接口只有一个方法handle()
,该方法接收HttpServletRequest
、HttpServletResponse
和AccessDeniedException
三个参数。在handle()
方法中,可以自定义响应的内容,例如返回一个自定义的错误页面或JSON
响应。
创建AccessDeniedHandlerImpl
类并实现AccessDeniedHandler
接口,实现自定义的JSON
响应。例如:
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
int code = 500;
response.setStatus(200);
response.setContentType("application/json;charset=UTF-8");
String msg = "权限不足,无法访问系统资源";
MapString, Object> result = new HashMap>();
result.put("msg", msg);
result.put("code", code);
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
}
}
然后,将自定义的accessDeniedHandler
注入到Spring Security
的配置中:
@Autowired
private AccessDeniedHandlerImpl accessDeniedHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return
http
// ...
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
// ...
}
这样,当访问被拒绝时,Spring Security
就会调用自定义AccessDeniedHandler
来处理。
②AuthenticationEntryPoint
AuthenticationEntryPoint
是Spring Security中的一个接口,用于定义如何处理未经身份验证的请求。当用户尝试访问需要身份验证的资源但未进行身份验证时,AuthenticationEntryPoint
将被调用。
在这个接口中,可以自定义如何处理这些未经身份验证的请求,例如重定向到登录页面或返回错误消息。需要注意的是,AuthenticationEntryPoint
只处理未经身份验证的请求,已经进行身份验证但权限不足的请求则需要使用AccessDeniedHandler
来处理。
创建AuthenticationEntryPointImpl
类并实现AuthenticationEntryPoint
接口,实现自定义的JSON
响应。例如:
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setStatus(200);
int code = 500;
String msg = "认证失败,无法访问系统资源";
response.setContentType("application/json;charset=UTF-8");
MapString, Object> result = new HashMap>();
result.put("msg", msg);
result.put("code", code);
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
}
}
然后,将自定义的authenticationEntryPoint
注入到Spring Security
的配置中:
@Autowired
private AuthenticationEntryPointImpl authenticationEntryPoint;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return
http
// ...
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
// ...
}
这样,当认证失败时,Spring Security
就会调用自定义AuthenticationEntryPoint
来处理。