本篇为大家带来Spring security的授权,首先要理解一些概念,有关于:权限、角色、安全上下文、访问控制表达式、方法级安全性、访问决策管理器
Spring Security 中的授权分为两种类型:
基于角色的授权:以用户所属角色为基础进行授权,如管理员、普通用户等,通过为用户分配角色来控制其对资源的访问权限。
基于资源的授权:以资源为基础进行授权,如 URL、方法等,通过定义资源所需的权限,来控制对该资源的访问权限。
Spring Security 提供了多种实现授权的机制,最常用的是使用基于注解的方式,建立起访问资源和权限之间的映射关系。
其中最常用的两个注解是 @Secured 和 @PreAuthorize。@Secured 注解是更早的注解,基于角色的授权比较适用,@PreAuthorize 基于 SpEL 表达式的方式,可灵活定义所需的权限,通常用于基于资源的授权。
使用SQL语句的方式查询该角色的权限,并且可以对它进行修改
根据用户id查询出对应的角色信息
SELECT * FROM sys_user a, sys_user_role b, sys_role_module c, sys_module d WHERE a.id = b.user_id and b.role_id=c.role_id and c.module_id = d.id and a.id=#{id}
根据用户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
但是并不推荐使用这种方法,当我们在实际开发中,要考虑到不同的数据表可能来自不同的库中,使用SQL查询时就会出现链表查询不同库的表的情况,所以,更多的时候我们会使用Java利用不同的操作对表进行依次查询作为条件最终得到结果
@Service public class UserServiceImpl extends ServiceImplimplements IUserService, UserDetailsService { @Autowired private IUserRoleService userRoleService; @Autowired private IRoleService roleService; @Autowired private IRoleModuleService roleModuleService; @Autowired private IModuleService moduleService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = getOne(new QueryWrapper ().eq("username", username)); if(user==null){ throw new UsernameNotFoundException("用户名无效"); } //查询出身份 //map遍历所有对象,返回新的数据放到新的集合中 //filter 过滤流中的内容 //collect将流中的元素变成一个集合 List role_ids = userRoleService .list(new QueryWrapper ().eq("user_id", user.getId())) .stream().map(UserRole::getRoleId) .collect(Collectors.toList()); //根据身份字段查询身份对应的名字字段 List roles = roleService .list(new QueryWrapper ()) .stream().map(Role::getRoleName) .collect(Collectors.toList()); //根据身份id查询具备的权限id List module_ids = roleModuleService .list(new QueryWrapper ().in("role_id", role_ids)) .stream().map(RoleModule::getModuleId) .collect(Collectors.toList()); //根据权限id查询对应的权限 List modules = moduleService .list(new QueryWrapper ().in("id", module_ids)) .stream().map(Module::getUrl) .collect(Collectors.toList()); // 将权限字段加到身份中 roles.addAll(modules); //将当前集合内容加到权限字段中 List authorities = roles.stream() .map(SimpleGrantedAuthority::new) .filter(Objects::nonNull) .collect(Collectors.toList()); user.setAuthorities(authorities); return user; } }
当我们想要开启spring方法级安全时,只需要在任何 @Configuration实例上使用@EnableGlobalMethodSecurity 注解就能达到此目的。同时这个注解为我们提供了prePostEnabled 、securedEnabled 和 jsr250Enabled 三种不同的机制来实现同一种功能。
修改WebSecurityConfig配置类,开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认。
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig { @Autowired private UserServiceImpl userDetailsService; @Autowired private ObjectMapper objectMapper; @Autowired private MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager() throws Exception { //创建DaoAuthenticationProvider DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); //设置userDetailsService,基于数据库方式进行身份认证 provider.setUserDetailsService(userDetailsService); //配置密码编码器 provider.setPasswordEncoder(passwordEncoder()); return new ProviderManager(provider); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeRequests() //antMatchers 匹配对应的路径 //permitAll 允许 .antMatchers("/").permitAll() //anyRequest 其余所有请求 //authenticated 登录 .anyRequest().authenticated() .and() .formLogin() //loginPage 登录页面 .loginPage("/") //设置处理登录请求的接口 .loginProcessingUrl("/userLogin") //用户的数据的参数 .usernameParameter("username") .passwordParameter("password") //登录成功 .successHandler((req, resp, auth) -> { Object user = auth.getPrincipal(); objectMapper .writeValue(resp.getOutputStream(), JsonResponseBody.success(user)); }) //登录失败 .failureHandler(myAuthenticationFailureHandler) .and() .exceptionHandling() //权限不足 .accessDeniedHandler((req, resp, ex) -> { objectMapper .writeValue(resp.getOutputStream(), JsonResponseBody.other(JsonResponseStatus.NO_ACCESS)); }) //没有认证 .authenticationEntryPoint((req, resp, ex) -> { objectMapper .writeValue(resp.getOutputStream(), JsonResponseBody.other(JsonResponseStatus.NO_LOGIN)); }) .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/"); http.csrf().disable(); 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标准的注解之一,用于在方法执行之前对访问进行权限验证 |
@Controller public class IndexController { @RequestMapping("/") public String toLogin() { return "login"; } @RequestMapping("/userLogin") public String userLogin() { return "index"; } @RequestMapping("/index") public String toIndex() { return "index"; } @RequestMapping("/noAccess") public String noAccess() { return "accessDenied"; } @ResponseBody @RequestMapping("/order_add") @PreAuthorize("hasAuthority('order:manager:add')") public String order_add() { return "订单新增"; } @ResponseBody @PreAuthorize("hasAuthority('book:manager:add')") @RequestMapping("/book_add") public String book_add() { return "书本新增"; } }
在当前登录的用户必须拥有当前的权限字段才能进行访问,例如:book:manager:add
AccessDeniedHandler是Spring Security提供的一个接口,用于处理访问被拒绝的情况。当用户尝试访问受保护资源但没有足够的权限时,Spring Security会调用AccessDeniedHandler来处理这种情况。
AccessDeniedHandler接口只有一个方法handle(),该方法接收HttpServletRequest、HttpServletResponse和AccessDeniedException三个参数。在handle()方法中,可以自定义响应的内容,例如返回一个自定义的错误页面或JSON响应。
创建AccessDeniedHandlerImpl类并实现AccessDeniedHandler接口,实现自定义的JSON响应。例如:
package com.yu.security.resp; import lombok.Getter; @Getter public enum JsonResponseStatus { OK(200, "OK"), UN_KNOWN(500, "未知错误"), RESULT_EMPTY(1000, "查询结果为空"), NO_ACCESS(3001, "没有权限"), NO_LOGIN(4001, "没有登录"), LOGIN_FAILURE(5001, "登录失败"), ; private final Integer code; private final String msg; JsonResponseStatus(Integer code, String msg) { this.code = code; this.msg = msg; } }
单独写一个接口进行实现,并将出现异常后的操作在里面实现
package com.yu.security.config; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.fasterxml.jackson.databind.ObjectMapper; import com.yu.security.pojo.User; import com.yu.security.resp.JsonResponseBody; import com.yu.security.resp.JsonResponseStatus; import com.yu.security.service.IUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Autowired private ObjectMapper objectMapper; // 在redis中定义一个键当登录失败是就对那个键的值进行加一 //如果大于三就锁住 @Autowired private IUserService userService; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { if(1==2){ User user = userService.getOne(new QueryWrapper().eq("username", request.getParameter("username"))); user.setAccountNonLocked(false); userService.updateById(user); } objectMapper.writeValue(response.getOutputStream(), JsonResponseBody.other(JsonResponseStatus.LOGIN_FAILURE)); } }
在当前例子中:我们通过在配置类引入当前接口,并实现当前接口,在实现类中,对登录失败进行 对应的操作,在Redis中定义一个键当登录失败是就对那个键的值进行加一,如果大于三就对当前账号进行冻结