关闭页面特效

Spring Security

0|1Spring Security和Shiro


框架名称 特点 应用 常用组合
Spring Security 功能更丰富,社区资源丰富 中大型的项目 Spring Boot/Spring Cloud + Spring Security
Shiro 上手更加的简单 小项目 SSM + Shiro

0|1认证和授权


认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户 授权:经过认证后判断当前用户是否有权限进行某个操作

在Spring Security中就具体化为用户认证(Authentication)和用户授权(Authorization)两个部分。

用户认证

验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。 用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。 通俗的说就是系统认为用户是否能登录。

用户授权

验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。 比如对某一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。 一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。 通俗点讲就是系统判断用户是否有权限去做某些事情

0|1SpringBoot整合SpringSecurity


image-20230726092135439

image-20230729095324464

启动项目,控制台中会打印出来密码:

image-20230726092246996

我们在浏览器中输入http://localhost:8080/user/list

直接跳转到http://localhost:8080/login

image-20230726092310004

输入完毕后点击Sign in 说明登录成功 跳转到http://localhost:8080/user/list

运行出结果

修改用户名和密码

spring.security.user.name=root spring.security.user.password=123456

当前用户登录后,有没有权限去访问相应的请求

授权

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; /*开启安全管理配置*/ @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override//自定义认证 protected void configure(HttpSecurity http) throws Exception { //authorizeRequests项目中的所有controller请求 //antMatchers不需要认证 //.permitAll();任何人都可以所以访问 http.authorizeRequests().antMatchers("/login").permitAll(); //验证角色商家可以发布商品 //http.authorizeRequests().antMatchers("/admin/pushGoods").hasRole("shangjia"); //验证有发布商品权限的所以用户 http.authorizeRequests().antMatchers("/admin/add","/delete","/pushGoods").hasAuthority("shangjiaquanxian"); //其他请求需要登录后访问 http.authorizeRequests().anyRequest().authenticated(); //目前使用表单登录 http.formLogin(); } }

0|1springsecurity的认证授权流程


Spring Security最核心的东西是一个过滤器链,这些过滤器在Spring boot启动的时候会帮我们配置上。

image-20230726092906557

执行流程

image-20230726111837171

文字描述话术:
具体的执行流程其实是一个过滤链:

username=root&&password=123456 1.UsernamePasswordAuthenticationFilter 拦截 /login post 从这个请求中 根据username 取出了用户名 root 根据password 取出了密码 123456 Authentication a1 = new Authentication(a1,setUsername("root")) a1.setPassword("123456) 2.在第一步的方法中 我们继续调用ProviderManager.authenticate(a1 ); 3.因为我们是表单提交 所以第三步 ProviderManager.authenticate(a1); 在authenticate内部继续调用DaoAuthenticationProvider.authencate(a1) 4.我们会继续调用InMemoryUserDetailsManager的loadUserByUsername(a1.username) 这个loadUserByUsername方法的返回值中包含 这个用户的用户名 密码 权限 角色信息 这个信息存放在一个叫UserDetails对象 这个对象中有当前登录这个用户的用户名root 密码123456对应的密码 权限角色信息 5.把a1中登录的密码 加密一下和 UserDetails中的密码进行比较 如果一样登录成功否则就是登录失败 6.如果登录成功 我们需要把UserDetails中的权限信息复制一份到a1对象中 返回a1对象 7.把a1对象存放在一个SecurityContext的上下文中?把a1对象存放到session中

1、用户向应用程序发起请求,请求需要经过Spring Security的过滤器链。
2、过滤器链首先会经过UsernamePasswordAuthenticationFilter过滤器,该过滤器判断请求是否是一个认证请求(如何知道是一个认证请求 拦截 对/login 的 POST 请求做拦截,校验表单中用户名,密码)。如果是认证请求,过滤器将获取请求中的用户名和密码,然后使用AuthenticationManager进行身份认证。
3、AuthenticationManager会根据用户名和密码创建一个Authentication对象,并将该对象传递给AuthenticationProvider进行认证。
4、AuthenticationProvider会根据传递过来的Authentication对象进行身份认证,并返回一个认证成功或失败的结果。
5、如果认证成功,UsernamePasswordAuthenticationFilter会将认证信息封装成一个Authentication对象,并将其放入SecurityContextHolder上下文中。

SecurityContextHolder

SecurityContextHolder上下文

session 一个用户在服务器上有一片空间 我们可以向这个空间中存放数据 只要是这个用户发送的请求 都可以共享这个空间的数据

SecurityContextHolder 获取securityContextSecurityContextHolder getContext() 方法 SecurityContext securityContext = SecurityContextHolder.getContext(); 1 securityContext获取Authentication Authentication authentication = securityContext.getAuthentication() 1 获取用户的信息,也就是UserDetails UserDetails principal = (UserDetails)authentication.getPrincipal();

SecurityContextHolder 用来获取登录之后用户信息。Spring Security 会将登录用户数据保存在 Session 中。但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,Spring Security 会将登录成功的用户信息保存到 SecurityContextHolder 中。SecurityContextHolder 中的数据保存默认是通过ThreadLocal 来实现的,使用 ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContexHolder 中的数据清空。以后每当有请求到来时,Spring Security 就会先从 Session 中取出用户登录数据,保存到 SecurityContextHolder 中,方便在该请求的后续处理过程中使用,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将 Security SecurityContextHolder 中的数据清空。这一策略非常方便用户在 Controller、Service 层以及任何代码中获取当前登录用户数据。

6、用户请求获取资源时,会经过FilterSecurityInterceptor过滤器,该过滤器会根据请求的URL和HTTP方法获取访问控制列表(Access Control List)。
7、Access Control List会包含访问资源所需要的权限信息,FilterSecurityInterceptor会将Authentication对象和Access Control List传递给AccessDecisionManager进行授权决策。
8、AccessDecisionManager会调用多个AccessDecisionVoter进行投票,并根据投票结果来决定当前用户是否有访问该资源的权限。如果用户被授权访问资源,应用程序将返回资源的响应结果。

总结就是首先经过认证过滤器实现认证,认证成功的话就会将用户信息存到authentication对象里面放到security上下文去(后续的权限校验需要获取到),这里面是包括权限的,之后再由AccessDecisionManager去根据相关策略进行权限鉴定

UsernamePasswordAuthenticationFilter 在org.springframework.security.web.authentication包下 可以查看源码

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { 这也是第一个过滤器 过滤器来了先走这个方法 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } else { String username = this.obtainUsername(request); username = username != null ? username : ""; username = username.trim(); String password = this.obtainPassword(request); password = password != null ? password : ""; UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } } protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { SecurityContextHolder.getContext().setAuthentication(authResult);// 将登录成功信息存放在 if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult)); } this.rememberMeServices.loginSuccess(request, response, authResult); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } this.successHandler.onAuthenticationSuccess(request, response, authResult); } protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); this.logger.trace("Failed to process authentication request", failed); this.logger.trace("Cleared SecurityContextHolder"); this.logger.trace("Handling authentication failure"); this.rememberMeServices.loginFail(request, response); this.failureHandler.onAuthenticationFailure(request, response, failed); } }

ProviderManager

ProviderManager 在 org.springframework.security.authentication包下 可以查看源码 他实现了AuthenticationManager这个接口

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; int currentPosition = 0; int size = this.providers.size(); Iterator var9 = this.getProviders().iterator(); while(var9.hasNext()) { AuthenticationProvider provider = (AuthenticationProvider)var9.next(); if (provider.supports(toTest)) { if (logger.isTraceEnabled()) { Log var10000 = logger; String var10002 = provider.getClass().getSimpleName(); ++currentPosition; var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size)); } try { result = provider.authenticate(authentication); if (result != null) { this.copyDetails(authentication, result); break; } } catch (InternalAuthenticationServiceException | AccountStatusException var14) { this.prepareException(var14, authentication); throw var14; } catch (AuthenticationException var15) { lastException = var15; } } } if (result == null && this.parent != null) { try { parentResult = this.parent.authenticate(authentication); result = parentResult; } catch (ProviderNotFoundException var12) { } catch (AuthenticationException var13) { parentException = var13; lastException = var13; } } if (result != null) { if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) { ((CredentialsContainer)result).eraseCredentials(); } if (parentResult == null) { this.eventPublisher.publishAuthenticationSuccess(result); } return result; } else { if (lastException == null) { lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}")); } if (parentException == null) { this.prepareException((AuthenticationException)lastException, authentication); } throw lastException; } } }

因为是表单提交 所以调用的是DaoAuthenticationProvider

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> { return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"); }); String username = this.determineUsername(authentication); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); } catch (UsernameNotFoundException var6) { this.logger.debug("Failed to find user '" + username + "'"); if (!this.hideUserNotFoundExceptions) { throw var6; } throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { this.preAuthenticationChecks.check(user); this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } catch (AuthenticationException var7) { if (!cacheWasUsed) { throw var7; } cacheWasUsed = false; user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); this.preAuthenticationChecks.check(user); this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } this.postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } return this.createSuccessAuthentication(principalToReturn, authentication, user); } protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { this.prepareTimingAttackProtection(); try { UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation"); } else { return loadedUser; } } catch (UsernameNotFoundException var4) { this.mitigateAgainstTimingAttack(authentication); throw var4; } catch (InternalAuthenticationServiceException var5) { throw var5; } catch (Exception var6) { throw new InternalAuthenticationServiceException(var6.getMessage(), var6); } } }

核心组件介绍:

Authentication
Authentication是一个接口,用来表示用户认证信息。
该对象主要包含了用户的详细信息(UserDetails)和用户鉴权时所需要的信息,如用户提交的用户名密码、Remember-me Token,或者digest hash值等。按不同鉴权方式使用不同的Authentication实现。
在用户登录认证之前相关信息会封装为一个Authentication具体实现类的对象,在登录认证成功之后又会生成一个信息更全面,包含用户权限等信息的Authentication对象,然后把它保存在 SecurityContextHolder所持有的SecurityContext中,供后续的程序进行调用,如访问权限的鉴定等。

接口中的方法:
从这个接口中,我们可以得到用户身份信息,密码,细节信息,认证信息,以及权限列表,具体的详细解读如下:

getAuthorities(): 用户权限信息(权限列表),通常是代表权限的字符串列表;

getCredentials(): 用户认证信息(密码信息),由用户输入的密码凭证,认证之后会移出,来保证安全性;

getDetails(): 细节信息,Web应用中一般是访问者的ip地址和sessionId;

getPrincipal(): 用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (UserDetails也是一个接口,里边的方法有getUsername,getPassword等);

isAuthenticated: 获取当前 Authentication 是否已认证;

setAuthenticated: 设置当前 Authentication 是否已认证(true or false)。

官方文档里说过,当用户提交登陆信息时,会将用户名和密码进行组合成一个实例UsernamePasswordAuthenticationToken,而这个类是Authentication的一个常用的实现类,用来进行用户名和密码的认证,类似的还有RememberMeAuthenticationToken,它用于记住我功能。

GrantedAuthority
Authentication的getAuthorities()方法返回一个 GrantedAuthority 对象数组。

GrantedAuthority该接口表示了当前用户所拥有的权限(或者角色)信息,用于配置 web授权、方法授权、域对象授权等。该属性通常由UserDetailsService 加载给 UserDetails。这些信息由授权负责对象AccessDecisionManager来使用,并决定最终用户是否可以访问某资源(URL或方法调用或域对象)。鉴权时并不会使用到该对象。
如果一个用户有几千个这种权限,内存的消耗将会是非常巨大的。

5.UserDetails
UserDetails存储的就是用户信息,它和Authentication接口类似,都包含了用户名,密码以及权限信息。

而区别就是Authentication中的getCredentials来源于用户提交的密码凭证,而UserDetails中的getPassword取到的则是用户正确的密码信息,认证的第一步就是比较两者是否相同,除此之外,Authentication#getAuthorities是认证用户名和密码成功之后,由UserDetails#getAuthorities传递而来。而Authentication中的getDetails信息是经过了AuthenticationProvider认证之后填充的。

其接口方法含义如下:

getAuthorites:获取用户权限,本质上是用户的角色信息。

getPassword: 获取密码。

getUserName: 获取用户名。

isAccountNonExpired: 账户是否过期。

isAccountNonLocked: 账户是否被锁定。

isCredentialsNonExpired: 密码是否过期。

isEnabled: 账户是否可用。

0|1完成自定义的认证授权流程


1.UsernamePasswordAuthenticationFilter这个过滤器如何知道我们提交的是登录操作,

2.去数据库验证用户名和密码的操作应该写在什么地方?

我们需要进行自定义配置

package com.tyhxzy.springsecurity.config; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import javax.annotation.Resource; // 开启安全框架 @EnableWebSecurity // 针对方法开启方法前和方法后的权限验证还有 角色认证 @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private UserDetailsService userDetailsService; /** * 配置密码解析 * @return */ @Bean protected PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 配置用户名和密码 * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { //关闭csrf防护 跨站请求防护 http.csrf().disable() //表单登录 .formLogin() //登录访问路径,与页面表单提交路径一致 .loginProcessingUrl("/login") .and() //认证配置 .authorizeRequests() .antMatchers("/login").permitAll() //任何请求 .anyRequest() //都需要身份验证 .authenticated(); //配置退出 http.logout() //退出路径 .logoutUrl("/logout") ; } }

导入以下依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.2</version> </dependency>

五张表

image-20230726093119359

登录验证的是t_user表

权限和角色分布在t_role和t_permission表中

实体类的创建(略 )自行完成

对应的mapper文件和映射文件的创建(略)

表数据请参考资料中的offcnpe.sql文件

业务逻辑层的编写如下

我们自定义一个业务逻辑层实现类

package com.tyhxzy.springsecurity.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.tyhxzy.springsecurity.entity.*; import com.tyhxzy.springsecurity.mapper.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; 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 java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @Service public class UserServiceImpl implements UserDetailsService { @Autowired private TUserMapper userMapper; @Autowired private TUserRoleMapper tUserRoleMapper; @Autowired private TRoleMapper roleMapper; @Autowired private TRolePermissionMapper tRolePermissionMapper; @Autowired private TPermissionMapper tPermissionMapper; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { LambdaQueryWrapper<TUser> userquery = new LambdaQueryWrapper<>(); userquery.eq(TUser::getUsername,s); TUser user = userMapper.selectOne(userquery); // 存放权限的集合 List<GrantedAuthority> ssp = null; if(user==null){ throw new UsernameNotFoundException("用户名不存在"); }else{ ssp = new ArrayList<>(); LambdaQueryWrapper<TUserRole> q2 = new LambdaQueryWrapper<>(); q2.eq(TUserRole::getUserId,user.getId()); List<TUserRole> rids = tUserRoleMapper.selectList(q2); // 遍历中间表 把所有的角色id查询回来 List<Integer> collect = rids.stream().map(sp -> sp.getRoleId()).collect(Collectors.toList()); List<TRole> tRoles = roleMapper.selectBatchIds(collect); Stream<SimpleGrantedAuthority> simpleGrantedAuthorityStream = tRoles.stream().map(sp1 -> { return new SimpleGrantedAuthority(sp1.getKeyword()); }); List<SimpleGrantedAuthority> collect1 = simpleGrantedAuthorityStream.collect(Collectors.toCollection(ArrayList::new)); ssp.addAll(collect1); // 查询权限 // 也是先查询中间表 for(TRole item:tRoles) { LambdaQueryWrapper<TRolePermission> q3 = new LambdaQueryWrapper<>(); q3.eq(TRolePermission::getRoleId,item.getId()); List<TRolePermission> tRolePermissions = tRolePermissionMapper.selectList(q3); List<Integer> pids = tRolePermissions.stream().map(sp1 -> sp1.getPermissionId()).collect(Collectors.toList()); List<TPermission> tps = tPermissionMapper.selectBatchIds(pids); Stream<SimpleGrantedAuthority> rty = tps.stream().map(sp1 -> { return new SimpleGrantedAuthority(sp1.getKeyword()); }); List<SimpleGrantedAuthority> collect2 = rty.collect(Collectors.toCollection(ArrayList::new)); ssp.addAll(collect2); } } return new User(s,user.getPassword(),ssp); } }

controller上编写

package com.tyhxzy.springsecurity.controller; import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user") public class UserController { @RequestMapping("/list") @Secured(value = "ROLE_ADMIN")// 访问这个方法之前首先验证这个用户是否是ROLE_ADMIN这个角色 public String hh(){ return "哈哈"; } @RequestMapping("/list1") @PreAuthorize("hasAuthority('CHECKITEM_QUERY')") // 访问这个方法前首先验证这个用户是否有CHECKITEM_QUERY的权限 public String hh1(){ return "哈哈"; } }

在浏览器中访问http://localhost:8080/user/list 因为未登录会直接被spring security打到登录页面,

在登录页面输入 admin 密码是123456 这个人的角色时ROLE_ADMIN可以直接在页面输出哈哈

如果在登录页面输入 xiaoming 密码是123456 这个人的角色不是ROLE_ADMIN 所以会跳转到403页面

自定义登录失败和权限认证失败的内容返回给客户端,不要直接打印出来403页面或者登录失败

AccessDeineHandler 用来解决认证过的用户访问无权限资源时的异常

package com.tyhxzy.springsecurity.config; 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; /** * 认证失败处理类 返回未授权 * 用来解决认证过的用户访问无权限资源时的异常 */ @Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setCharacterEncoding("utf-8"); response.setContentType("application/json"); response.getWriter().print("没有访问权限!"); } } WebSecurityConfig中添加如下代码: @Autowired private CustomAccessDeniedHandler deniedHandler; // 设置已经登录过 但是没有权限访问要走的对象 http.exceptionHandling().accessDeniedHandler(deniedHandler);

AuthenticationFailureHandler 用来解决登录失败的异常

package com.tyhxzy.springsecurity.config; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; 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; import java.util.HashMap; import java.util.Map; @Component public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException, IOException { Map<String, Object> result = new HashMap<String, Object>(); result.put("msg", "登录失败: "+exception.getMessage()); result.put("status", 500); response.setContentType("application/json;charset=UTF-8"); String s = new ObjectMapper().writeValueAsString(result); response.getWriter().println(s); } } WebSecurityConfig中添加如下代码: @Autowired @Autowired private MyAuthenticationFailureHandler aa; http.csrf().disable() //表单登录 .formLogin() //登录访问路径,与页面表单提交路径一致 .loginProcessingUrl("/login").failureHandler(aa)

0|1总结


常见的过滤器

image-20230727153431606

【1】WebAsyncManagerIntegrationFilter
将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。

【2】SecurityContextPersistenceFilter
在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。

【3】HeaderWriterFilter
用于将头信息加入响应中。

【4】CsrfFilter
用于处理跨站请求伪造。

【5】LogoutFilter
用于处理退出登录。

【6】UsernamePasswordAuthenticationFilter
用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改

【7】DefaultLoginPageGeneratingFilter
如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。

【8】BasicAuthenticationFilter
检测和处理 http basic 认证。

【9】RequestCacheAwareFilter
用来处理请求的缓存。

【10】SecurityContextHolderAwareRequestFilter
主要是包装请求对象request。

【11】AnonymousAuthenticationFilter
检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。

【12】SessionManagementFilter
管理 session 的过滤器。

【13】ExceptionTranslationFilter
处理 AccessDeniedException 和 AuthenticationException 异常。该过滤器不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。

【14】FilterSecurityInterceptor
可以看做过滤器链的出口。该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并由ExceptionTranslationFilter过滤器进行捕获和处理。

【15】RememberMeAuthenticationFilter
当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

image-20230731093721767

0|1常见的验证授权流程


一、Session-Cookie 机制 (web应用中最常见的)
当服务端需要对访问的客户端进行身份认证时,常用的做法是通过session-cookie 机制流程

image-20230726093141123

Session-Cookie 认证存在的问题:

当客户访问量增加,服务端需要存储大量的session会话,对服务端有很大考验
当服务端为集群时,用户登录其中一台服务器,会将session保存在该服务器的内存中,
但是当用户访问其他服务器时。会无法访问。(已经有了成熟的解决方案)可以采用使用缓存服务器来保证共享 第三方缓存来保存session由于依赖cookie,所以存在CSRF安全问题

前后端分离项目不共享session 演示问题

//关闭csrf防护 跨站请求防护 http.csrf().disable(); http //认证配置 .authorizeRequests() .antMatchers("/user/login","/user/add").permitAll(); http.authorizeRequests().anyRequest().authenticated(); http.cors();// 开启跨域 // 设置登录失败的对象 //配置退出 http.logout() //退出路径 .logoutUrl("/logout") ; //http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

二、Token 认证机制:

//http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

使用Jwt

0|1跨域概念


跨域是如何引起的? 浏览器的安全策略 http://localhost:8080/index.html http://localhost:8082/demo.html 协议:http localhost:主机名 ip:127.0.0.1 端口号:8080 协议 ip 端口号 三者都一致 叫同源 浏览器默认只允许该项目只能访问 和自己协议 ip 端口号一致的项目中的资源 http://localhost:8080/index.html 访问和自己 同源的demo.html是允许的 http://localhost:8080/index.html 访问 http://localhost:8082/demo.html 这就叫跨域 我们昨天是在 http://localhost:8080/index.html 去请求 http://localhost:8081/demo/a1 这默认浏览器是不允许的 这就叫跨域 Access to XMLHttpRequest at 'http://localhost:8081/demo/a1' from origin 'http://localhost:8080' has been blocked by CORS policy origin 'http://localhost:8080' 从这个项目 请求 ttp://localhost:8081/demo/a1 另外一个项目 has been blocked by CORS policy 被跨域政策拦截住 No 'Access-Control-Allow-Origin' header is present on the requested resource. 在请求头上我们没找见 Access-Control-Allow-Origin @CrossOrigin:告知该controller中的所有请求 允许任何项目访问(不限制必须是同源) @CrossOrigin(origins = "http://localhost:8080") 告知该controller请求只能由 http://localhost:8080这个源访问 如果你在controller类上写了@CrossOrigin 那么服务器会向客户端的响应头中写入Access-Control-Allow-Origin=“*” 不限制源 如果你在controller类上写了@CrossOrigin(origins = "http://localhost:8080") 那么服务器会向客户端响应头中写入Access-Control-Allow-Origin=“http://localhost:8080” 浏览器拿到这个Access-Control-Allow-Origin=“http://localhost:8080” 之后 会进行比对 看自己在不在人家的范围内 如果不在 还是被跨域拦截,如果在 就说明人家允许你进行访问 浏览器先根据同源策略对前端页面和后台交互地址做匹配,若同源,则直接发送数据请求:若不同源,则发送跨域请求°。 。当我们发起跨域请求时,如果是非简单请求,浏览器会帮我们自动触发预检请求,也就是OPTIONS请求,用于确认目标资源是否支持跨域。如果是简单请求,则不会触发预检,直接发出正常请求。 ·服务器收到浏览器跨域请求后,根据自身配置返回对应文件头。若未配置过任何允许跨域,则文件头里不包含Access-Control-All ow-origin字段,若配置过域名,则返回 Ac cess-Control-Allow-origin+对应配置规则里的域名的方式。 ●浏览器根据接收到的响应头里的Access-Con trol-Allow-origin字段做匹配,若无该字段,说明不允许跨域,从而抛出一个错误;若有该字段,则对字段内容和当前域名做比对,如果同源,则说明可以跨域,浏览器接受该响应;若不同源,则说明该域名不可跨域,浏览器不接受该响应,并抛出一个错误。

0|1跨域问题


spring security 框架CsrfFilter权限高于@CrossOrigin

需要在配置类中关闭spring security的跨域保护

http.cors();

关闭表单登录

//http:formLogin.failureHandler();

配置放行界面

http.authorizeRequests().antMatchers("/demo/a1").permitAll();
http.csrf().disable();//关闭跨站请求伪造

告诉spring security不需要再把认证过的数据往session存放

http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

0|1自定义过滤器


表单登录失败

class ForwardAuthenticationFailureHandler implements AuthenticationFailureHandler
package com.mmkj.offcnpe.config; import com.fasterxml.jackson.databind.ObjectMapper; 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; import java.util.HashMap; /** * 适用于表单登录失败 */ @Component public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json"); HashMap<String, Object> stringObjectHashMap = new HashMap<String, Object>(); stringObjectHashMap.put("message","登录失败"); stringObjectHashMap.put("code",2002); //httpServletResponse.sendRedirect("/offcnpe/checkgroup/findAllCheckItems"); httpServletResponse.getWriter().print( new ObjectMapper().writeValueAsString(stringObjectHashMap)); } }

已登陆权限不足跳转

package com.mmkj.offcnpe.config; 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; //已登陆权限不足跳转 @Component public class MyCustomAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json"); HashMap<String, Object> stringObjectHashMap = new HashMap<String, Object>(); stringObjectHashMap.put("message","权限不足"); stringObjectHashMap.put("code",2001); httpServletResponse.getWriter().print( new ObjectMapper().writeValueAsString(stringObjectHashMap)); } }

非表单登陆失败

package com.mmkj.offcnpe.config; 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; //非表单登陆失败 @Component public class MySecurity403 implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json"); HashMap<String, Object> stringObjectHashMap = new HashMap<String, Object>(); stringObjectHashMap.put("message","权限不足"); stringObjectHashMap.put("code",2003); httpServletResponse.getWriter().print( new ObjectMapper().writeValueAsString(stringObjectHashMap)); } }

表单登录成功处理器

class ForwardAuthenticationSuccessHandler implements AtuhenticationSuccessHandler

token验证登录状态

package com.mmkj.offcnpe.config; import io.jsonwebtoken.Jwts; import lombok.SneakyThrows; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.stream.Collectors; //spring的过滤器,每次请求都进行拦截 @Component public class OncePerOverRequestFilter extends OncePerRequestFilter { @Autowired private RedisTemplate<String, Object> template; @SneakyThrows @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { String auth = httpServletRequest.getHeader("auth");//获取请求头中加密信息 if (auth != null && auth.equals("")) { auth = auth.substring("Bearer".length()); Object nameObj = Jwts.parser().setSigningKey("yxh111").parseClaimsJws(auth).getBody().get("username");//解密获取用户名称 if (nameObj != null) { LinkedHashMap<String, Object> redisDetail_Data = (LinkedHashMap<String, Object>) template.opsForValue().get(nameObj.toString());//authorties对象集合,格式不对 ArrayList<LinkedHashMap<String, String>> a2 = (ArrayList<LinkedHashMap<String, String>>) redisDetail_Data.get("authorities"); //List<GrantedAuthority> authority = AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", a2.stream().map(v -> v.get("authority")).collect(Collectors.toList()))); List<SimpleGrantedAuthority> collect = a2.stream().map(v -> v.get("authority")).map(v1 -> new SimpleGrantedAuthority(v1)).collect(Collectors.toList()); UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken(redisDetail_Data.get("username").toString(), redisDetail_Data.get("password").toString(), collect);//创建UsernamePasswordAuthenticationToken对象 SecurityContextHolder.getContext().setAuthentication(upat);//放入上下文 } } filterChain.doFilter(httpServletRequest, httpServletResponse);//放行执行链 } }

加载过滤器

package com.mmkj.offcnpe.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @EnableWebSecurity//开启安全管理配置 @EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private MyCustomAccessDeniedHandler myCustomAccessDeniedHandler; @Autowired private MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Autowired private OncePerOverRequestFilter oncePerOverRequestFilter; @Autowired private MySecurity403 mySecurity403; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder()); } @Override//自定义认证 protected void configure(HttpSecurity http) throws Exception { //http.formLogin().failureHandler(myAuthenticationFailureHandler);//表单登录失败跳转 http.authorizeRequests().antMatchers("/offcnpe/user/login").permitAll().anyRequest().authenticated();//放行请求 http.exceptionHandling().accessDeniedHandler(myCustomAccessDeniedHandler);//已登陆权限不足 http.exceptionHandling().authenticationEntryPoint(mySecurity403);//未登录权限不足 http.csrf().disable();//关闭跨站请求伪造 http.cors ();//关闭跨域保护 http.addFilterAt(oncePerOverRequestFilter, UsernamePasswordAuthenticationFilter.class);//登录成功请求头中状态验证 http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//告诉spring security不需要再把认证过的数据往session存放 } }

controller中登录判定

package com.mmkj.offcnpe.controller; import com.mmkj.offcnpe.entity.TUser; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Calendar; import java.util.Date; import java.util.HashMap; /** * <p> * 前端控制器 * </p> * * @author yxh * @since 2023-07-17 */ @RestController @RequestMapping("/offcnpe/user") @CrossOrigin public class UserController { @Qualifier("userdetails") @Autowired private UserDetailsService userDetailsService; @Autowired private RedisTemplate<String, Object> template; @RequestMapping("/login") public String login(@RequestBody TUser user) { if (!(SecurityContextHolder.getContext().getAuthentication() instanceof UsernamePasswordAuthenticationToken)){//判断当前上下文是否为空 BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername()); if (bCryptPasswordEncoder.matches(user.getPassword(), userDetails.getPassword())) { return "密码错误"; } else { template.opsForValue().set(user.getUsername(), userDetails); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); JwtBuilder builder = Jwts.builder(); Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.HOUR,1); builder.setId("idxxx"); builder.setIssuer("张三"); builder.setExpiration(calendar.getTime()); builder.setIssuedAt(new Date()); builder.signWith(SignatureAlgorithm.HS256,"yxh111"); HashMap<String, Object> stringObjectHashMap = new HashMap<>(); stringObjectHashMap.put("username",userDetails.getUsername()); builder.addClaims(stringObjectHashMap); return builder.compact(); } } return "登录成功"; } @RequestMapping("/admin") @Secured("ROLE_ADMIN") public String admin() { return "admin"; } @RequestMapping("/publicList") public String publicList() { return "user"; } @RequestMapping("/health") @Secured("ROLE_HEALTH_MANAGER") public String user() { return "health-manager"; } @RequestMapping("/delUser") @PreAuthorize("hasAuthority('USER_DELETE')") public String delUser() { return "delUser"; } }


__EOF__

作  者YXH
出  处https://www.cnblogs.com/YxinHaaa/p/17583550.html
关于博主:编程路上的小学生,热爱技术,喜欢专研。评论和私信会在第一时间回复。或者直接私信我。
版权声明:署名 - 非商业性使用 - 禁止演绎,协议普通文本 | 协议法律文本
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!

posted @   YxinHaaa  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
0
0
关注
跳至底部
点击右上角即可分享
微信分享提示