上海网站建设与设计公司,网站建设外包网,重庆在线高校,湖州做网站公司【SpringSecurity】认证授权框架——SpringSecurity使用方法 文章目录【SpringSecurity】认证授权框架——SpringSecurity使用方法1. 概述2. 准备工作2.1 引依赖2.2 测试3. 认证3.1 认证流程3.2 登录校验问题3.3 实现3.3.1 实现UserDetailsService接口3.3.2 密码存储和校验3.3.…【SpringSecurity】认证授权框架——SpringSecurity使用方法 文章目录【SpringSecurity】认证授权框架——SpringSecurity使用方法1. 概述2. 准备工作2.1 引依赖2.2 测试3. 认证3.1 认证流程3.2 登录校验问题3.3 实现3.3.1 实现UserDetailsService接口3.3.2 密码存储和校验3.3.3 自定义登录接口3.3.3.1 配置3.3.3.2 定义登录方法3.3.4 自定义认证过滤器3.3.4.1 确定过滤器顺序3.3.5 自定义退出登录接口4. 授权4.1 授权流程4.2 实现4.2.1 开启全局权限功能4.2.2 封装权限信息4.3 自定义权限校验方法4.4 基于配置的权限控制5. 自定义失败处理5.1 认证失败处理器5.2 权限不足处理器5.3 配置6. 跨域问题1. 概述
Spring Security是一个框架提供 认证authentication、授权authorization和保护以抵御常见的攻击。它对保护命令式和响应式应用程序有一流的支持是保护基于Spring的应用程序的事实标准。它还提供了与其他库的集成以简化其使用。
SpringSecurity的详细使用方法可见Spring Security 中文文档 :: Spring Security Reference (springdoc.cn)
写这篇文章主要是为了复习SpringSecurity的相关知识在这里也向大家推荐b站up主 三更草堂 他讲的SpringSecurity课程非常棒 2. 准备工作
2.1 引依赖
创建springboot工程中引入这些依赖
parentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.5.0/version
/parent
dependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdoptionaltrue/optional/dependency!--主要是这个依赖--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-security/artifactId/dependency
/dependencies2.2 测试
引入好依赖之后就可以启动应用了我们访问应用的任何一个接口都会跳转到SpringSecurity的一个默认登录页面(如下所示) SpringSecurity提供了一个默认的登录接口 /login 和退出接口 /logout 。登录接口的默认用户名为 user 而密码则打印在了应用的控制台 使用默认的用户名和控制台上的密码就能够登录系统访问接口了。在正常情况下我们需要禁用这个接口使用我们自定义的接口。 3. 认证
SpringSecurity的认证机制有很多比如
Username and Password使用用户名/密码进行认证OAauth 2.0 Login使用OpenID Connect 和非标准的OAuth 2.0登录即GitHub的OAuth 2.0登录。
我们主要了解第一种方式。SpringSecurity认证通过过滤器链实现 SpringSecurity的过滤器链包含15个过滤器上图只展示了较为核心的过滤器。
SecurityContextPersistenceFilter过滤器链的入口和出口保存和清除 SecurityContextHolder 中的 SecurityContext 。UsernamePasswordAuthenticationFilter负责收集我们在登陆页面输入的用户名和密码并封装成Authentication对象。ExceptionTranslationFilter只处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 其他异常继续抛出。FilterSecurityInterceptor负责权限校验的过滤器。 3.1 认证流程 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ailkTJHA-1680426554649)(null)]
注释
Authentication是一个接口它的实现类表示当前访问系统的用户封装了用户相关信息。AuthenticationManager也是一个接口声明了认证Authentication的方法UserDetailsService还是一个接口加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。UserDetails接口提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。 3.2 登录校验问题
登录流程 自定义登录接口替代原有登录接口。 自定义实现UserDetailService接口的类A替代原有实现类。 在类A中中查询数据库进行认证校验。 校验通过则使用jwt工具类生成tokentoken生成规则有两种 4.1 使用用户id生成token其余用户信息存入redis。 4.2 使用用户基本信息的json串(脱敏)生成token服务端不必存储用户信息。
校验流程 自定义jwt认证过滤器。 解析token。 2.1 解析获得用户id根据用户id从redis中获得用户信息存入SecurityContextHolder。 2.2 解析获得用户信息json串将其反序列化为用户对象存入SecurityContextHolder。
在这里我们采用第一种方案使用redis。 3.3 实现
3.3.1 实现UserDetailsService接口
创建UserDetailsService接口的实现类在其中对用户进行认证和授权。
Service
public class UserDetailServiceImpl implements UserDetailsService {Autowiredprivate UserMapper userMapper;Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//认证LambdaQueryWrapperUser queryWrapper new LambdaQueryWrapper();queryWrapper.eq(StringUtils.isNotBlank(username), User::getUserName, username);User user userMapper.selectOne(queryWrapper);if (Objects.isNull(user)) {throw new RuntimeException(用户名或密码错误);}//todo 查询对应的权限信息//把数据封装成UserDetails返回UserDetails userDetails org.springframework.security.core.userdetails.User.withUsername(user.getUserName()).password(user.getPassword()).authorities(new String[]{}).build();return userDetails;}
}除了使用security提供的生成UserDetails对象的方法外我们还可以选择自定义。
我们新创建一个类 LoginUser 实现 UserDetails 接口。
Data
AllArgsConstructor
NoArgsConstructor
public class LoginUser implements UserDetails {private User user;Overridepublic Collection? extends GrantedAuthority getAuthorities() {return null;}Overridepublic String getPassword() {return user.getPassword();}Overridepublic String getUsername() {return user.getUserName();}Overridepublic boolean isAccountNonExpired() {return true;}Overridepublic boolean isAccountNonLocked() {return true;}Overridepublic boolean isCredentialsNonExpired() {return true;}Overridepublic boolean isEnabled() {return true;}
}使用我们自定义的类去创建UserDetails对象。
Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//认证LambdaQueryWrapperUser queryWrapper new LambdaQueryWrapper();queryWrapper.eq(StringUtils.isNotBlank(username), User::getUserName, username);User user userMapper.selectOne(queryWrapper);if (Objects.isNull(user)) {throw new RuntimeException(用户名或密码错误);}//todo 查询对应的权限信息//把数据封装成UserDetails返回return new LoginUser(user);
}3.3.2 密码存储和校验
我们不会在数据库中使用明文的方式存储密码我们需要存储的是加密之后的密码。
SpringSecurity默认使用的PasswordEncoder要求数据库中的密码格式为{id}password 。它会根据id去判断密码的加密方式。这种方式不太行所以我们需要使用BCryptPasswordEncoder去替代原来的PasswordEncoder。
我们只需要定义一个SpringSecurity的配置类并且让这个类继承 WebSecurityConfigurerAdapter 。
Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {//创建 BCryptPasswordEncoder 对象并注入容器Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}3.3.3 自定义登录接口
创建一个控制器 LoginController 在其中定义一个登录接口
RestController
public class LoginController {Autowiredprivate UserService userService;PostMapping(/user/login)public ResponseResult login(RequestBody User user) {return userService.login(user);}
}3.3.3.1 配置
我们需要让SpringSecurity对自定义的接口放行让用户不用登陆也可以访问。然后在 login(user) 中调用AuthenticationManager的authenticate方法来进行用户认证所以需要在SpringSecurity配置类中进行以下两点配置
对自定义登录接口放行把 AuthenticationManager 对象注入容器
Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {//创建 BCryptPasswordEncoder 对象并注入容器Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}//注入AuthenticationManager对象OverrideBeanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}//对登录接口放行Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问(未登录时可以访问登陆后不能访问).antMatchers(/user/login).anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();}
}3.3.3.2 定义登录方法
在用户服务接口的实现类中定义 login(user) 方法
Service(userService)
public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService {Autowiredprivate StringRedisTemplate stringRedisTemplate;Autowiredprivate AuthenticationManager authenticationManager;Overridepublic ResponseResult login(User user) {//调用 AuthenticationManager 对象的 authenticate() 方法进行用户认证UsernamePasswordAuthenticationToken authenticationToken new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());Authentication authenticate null;try {authenticate authenticationManager.authenticate(authenticationToken);} catch (Exception e) {throw new RuntimeException(e.getMessage());}if (authenticate null) {throw new RuntimeException(登陆失败);}//如果认证未通过则给出对应提示//通过则使用userid生成一个jwtjwt存入ResponseResultLoginUser loginUser (LoginUser) authenticate.getPrincipal();String userId loginUser.getUser().getId().toString();String jwt JwtUtil.createJWT(userId);MapString, String map new HashMap();map.put(token, jwt);//把用户信息存入redisstringRedisTemplate.opsForValue().set(login: userId, JSON.toJSONString(loginUser));return new ResponseResult(200, 登录成功, map);}
}3.3.4 自定义认证过滤器
我们需要一个认证过滤器作用是取出每次请求的请求头看它是否携带 token 。再对 token 进行校验判断用户的登录状态。
Component
public class JwtAuthenticactionTokenFilter extends OncePerRequestFilter {Autowiredprivate StringRedisTemplate stringRedisTemplate;Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取tokenString token request.getHeader(token);if (StringUtils.isBlank(token)) {//没有token//放行让后面的认证过滤器去拒绝filterChain.doFilter(request, response);return;}//解析tokenString userId;try {Claims claims JwtUtil.parseJWT(token);userId claims.getSubject();} catch (Exception e) {e.printStackTrace();throw new RuntimeException(token非法);}//拼接redis的keyString key login: userId;String userJson stringRedisTemplate.opsForValue().get(key);if (StringUtils.isBlank(userJson)) {throw new RuntimeException(用户未登录);}LoginUser loginUser JSON.parseObject(userJson, LoginUser.class);//存入SecurityContextHolder//todo 还未授权UsernamePasswordAuthenticationToken authenticationToken new UsernamePasswordAuthenticationToken(loginUser, null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行filterChain.doFilter(request, response);}
}3.3.4.1 确定过滤器顺序
定义了认证过滤器后我们还需要确定过滤器的执行顺序我们必须保证这个过滤器的执行在Security的认证过滤器之前所以我们把他放在 UsernamePasswordAuthenticationFilter 之前比较合适。
找到之前的配置类从容器中取出自定义的 JwtAuthenticactionTokenFilter 过滤器并在 configure() 方法中加入最后一行代码。
Autowired
private JwtAuthenticactionTokenFilter jwtAuthenticactionTokenFilter;protected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问(未登录时可以访问登陆后不能访问).antMatchers(/user/login).anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();//把自定义认证过滤器放在 UsernamePasswordAuthenticationFilter 过滤波器前面http.addFilterBefore(jwtAuthenticactionTokenFilter, UsernamePasswordAuthenticationFilter.class);
}3.3.5 自定义退出登录接口
退出流程
通过 SecurityContextHolder 获得用户id利用用户id凭借redis的key值通过key值把redis中的用户信息删除
首先定义一个退出接口
RequestMapping(/user/logout)
public ResponseResult logout() {return userService.logout();
}在实现类中定义 logout() 方法
Override
public ResponseResult logout() {//获取SecurityContextHolder中的用户idAuthentication authentication SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser (LoginUser) authentication.getPrincipal();Long id loginUser.getUser().getId();//删除redis中的值stringRedisTemplate.delete(login: id);return new ResponseResult(200, 退出成功);
}4. 授权
4.1 授权流程
在SpringSecurity中会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。 4.2 实现
4.2.1 开启全局权限功能
EnableGlobalMethodSecurity(prePostEnabled true)开启功能之后我们就可以在接口方法上面加上 PreAuthorize 注解了。
RequestMapping(/hello)
PreAuthorize(hasAuthority(test))
public String hello() {return hello;
}4.2.2 封装权限信息
权限的封装是在登录流程时发生的所以我们在登录方法中把权限信息封装进入 UserDetails 的实现类中
Data
NoArgsConstructor
public class LoginUser implements UserDetails {private User user;//权限字符串集合private ListString permissions;//权限集合,不用序列化到redisJSONField(serialize false)private SetSimpleGrantedAuthority authorities;public LoginUser(User user, ListString permissions) {this.user user;this.permissions permissions;}//将权限字符串集合封装成权限集合Overridepublic Collection? extends GrantedAuthority getAuthorities() {if (authorities null) {authorities permissions.stream().map(item - new SimpleGrantedAuthority(item)).collect(Collectors.toSet());}return authorities;}Overridepublic String getPassword() {return user.getPassword();}Overridepublic String getUsername() {return user.getUserName();}Overridepublic boolean isAccountNonExpired() {return true;}Overridepublic boolean isAccountNonLocked() {return true;}Overridepublic boolean isCredentialsNonExpired() {return true;}Overridepublic boolean isEnabled() {return true;}
}修改完毕我们先在登录校验方法 loadUserByUsername(String username) 中查询数据库得到权限字符串集合
Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//认证LambdaQueryWrapperUser queryWrapper new LambdaQueryWrapper();queryWrapper.eq(StringUtils.isNotBlank(username), User::getUserName, username);User user userMapper.selectOne(queryWrapper);if (Objects.isNull(user)) {throw new RuntimeException(用户名或密码错误!);}//查询对应的权限信息ListString list menuMapper.selectPermsByUserId(user.getId());return new LoginUser(user, list);
}然后在自定义过滤器中补充权限设置
UsernamePasswordAuthenticationToken authenticationToken new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());4.3 自定义权限校验方法
Security提供的权限校验方法比较单一且不灵活。所以我们可以自定义权限校验方法并且在注解中使用我们的方法。
1自定义权限校验方法
创建一个类并注入容器中在其中编写我们的校验方法
Component(value ex)
public class ExpressionRoot {public boolean hasAuthority(String authority) {//获取当前用户的权限Authentication authentication SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser (LoginUser) authentication.getPrincipal();ListString permissions loginUser.getPermissions();//判断用户是否拥有访问权限return permissions.contains(authority);}
}2在注解中使用我们的方法
RequestMapping(/hello)
//PreAuthorize(hasAuthority(system:dept:list))
PreAuthorize(ex.hasAuthority(system:dept:list))
public String hello() {return hello;
}4.4 基于配置的权限控制
我们上面的示例都是基于注解的权限控制我们也可以通过配置来进行权限的控制如下所示 http.authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers(/user/login).anonymous()//基于配置的权限控制.antMatchers(/testCors).hasAuthority(system:dept:list222)// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();5. 自定义失败处理
我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。
在SpringSecurity中如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
所以如果我们需要自定义异常处理我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。 5.1 认证失败处理器
自定义认证失败处理器
Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {Overridepublic void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {ResponseResult result new ResponseResult(HttpStatus.UNAUTHORIZED.value(), 用户认证失败请重新登录);String json JSON.toJSONString(result);//处理异常WebUtils.renderString(httpServletResponse, json);}
}在 WebUtils 工具类中对响应进行封装具体方法如下
public static String renderString(HttpServletResponse response, String string) {try{response.setStatus(200);response.setContentType(application/json);response.setCharacterEncoding(utf-8);response.getWriter().print(string);}catch (IOException e){e.printStackTrace();}return null;
}5.2 权限不足处理器
自定义权限不足处理器
Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {Overridepublic void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {ResponseResult result new ResponseResult(HttpStatus.FORBIDDEN.value(), 权限不足);String json JSON.toJSONString(result);//处理异常WebUtils.renderString(httpServletResponse, json);}
}5.3 配置
我们将这两个处理器注入容器再将其配置到Security中
Autowired
private AuthenticationEntryPoint authenticationEntryPoint;Autowired
private AccessDeniedHandler accessDeniedHandler;//对登录接口放行
Override
protected void configure(HttpSecurity http) throws Exception {//省略其他配置......//配置异常处理器http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)//认证失败处理器.accessDeniedHandler(accessDeniedHandler);//授权失败处理器
}6. 跨域问题
只有浏览器和服务端的之间的请求有跨域问题服务端和服务端之间是不存在跨域问题的。
我们首先在我们的springboot项目中进行跨域配置
Configuration
public class CorsConfig implements WebMvcConfigurer {Overridepublic void addCorsMappings(CorsRegistry registry) {// 设置允许跨域的路径registry.addMapping(/**)// 设置允许跨域请求的域名.allowedOriginPatterns(*)// 是否允许cookie.allowCredentials(true)// 设置允许的请求方式.allowedMethods(GET, POST, DELETE, PUT)// 设置允许的header属性.allowedHeaders(*)// 跨域允许时间.maxAge(3600);}
}由于我们的资源都会收到SpringSecurity的保护所以想要跨域访问还要让SpringSecurity运行跨域访问。
然后在security的配置类中添加跨域配置
Override
protected void configure(HttpSecurity http) throws Exception {//省略其他配置......//允许跨域http.cors();
}