Spring Security学习笔记(二)Spring Security认证和鉴权
前言:本系列博客基于Spring Boot 2.6.x依赖的Spring Security5.6.x版本
上一篇博客介绍了Spring Security的整体架构,本篇博客要讲的是Spring Security的认证和鉴权两个重要的机制。
UsernamePasswordAuthenticationFilter和BasicAuthenticationFilter是用来认证的两个过滤器,FilterSecurityInterceptor是用来鉴权的。
一、Spring Security认证
Spring Security提供了许多认证机制,例如用户名密码认证、OAuth 2.0认证、SAML认证、Central Authentication Server (CAS)认证、Remember Me(记住过了session有效期的用户)、JAAS认证、X509认证等
1.1、认证架构
Spring Security认证架构主要由以下几个组件构成:
SecurityContext:Spring Security的上下文对象,包含了当前认证用户的Authentication(认证)。
SecurityContextHolder:用于设置和获取SecurityContext的静态工具类,保存了SecurityContext上下文对象。
Authentication:认证接口,定义了获取用户凭证、认证信息、权限等方法规范。
GrantedAuthority:权限类,用来定义用户的权限,Authentication中会保存一个GrantedAuthority类型的权限列表。
AuthenticationManager:认证管理器接口,只有一个authenticate方法,它的实现类实现该方法用来执行具体的认证逻辑,入参和出参都是Authentication。
ProviderManager:最常见的AuthenticationManager的实现。
AuthenticationProvider:认证功能提供者接口。在ProviderManager中实际上的认证逻辑由该接口的实现类处理。DaoAuthenticationProvider、AnonymousAuthenticationProvider都是它的实现类。
AuthenticationEntryPoint:用于从客户端请求凭证(即重定向到登录页面,返回需要登录响应等)。
AbstractAuthenticationProcessingFilter:一个用于认证的基本 Filter。是一个抽象类,只有UsernamePasswordAuthenticationFilter一个实现,UsernamePasswordAuthenticationFilter会从请求中获取username和 password参数,去进行认证。
1.1.1、SecurityContext
Spring Security的上下文对象,可以设置和获取Authentication认证信息。
public interface SecurityContext extends Serializable {// 获取Authentication对象Authentication getAuthentication();// 放入Authentication对象void setAuthentication(Authentication authentication);
}
1.1.2、SecurityContextHolder
SecurityContextHolder是用来设置和获取SecurityContext的静态工具类,SecurityContextHolder不关心SecurityContext里认证信息的细节,即Authentication的具体实现类型是什么它并不关心,如果它能获取到值,这个值就认为是当前用户的认证信息。
public class SecurityContextHolder {...//常用方法public static void clearContext() {strategy.clearContext();}public static SecurityContext getContext() {return strategy.getContext();}public static void setContext(SecurityContext context) {strategy.setContext(context);}...
}
SecurityContextHolder架构图:
默认情况下,SecurityContextHolder使用ThreadLocal来存储这些细节,这意味着 SecurityContext 对同一线程中的方法总是可用的,即使SecurityContext没有被明确地作为参数传递给这些方法。并且Spring Security的FilterChainProxy会确保SecurityContext总是被清空,不用我们手动清空。
1.1.3、Authentication
Authentication是认证信息接口,定义了获取用户凭证、认证信息、权限等方法规范。它主要有两个作用,一是充当未认证的用户凭证(包括用户名、密码);一是表示验证后的认证信息(包括认证后用户信息、用户权限等)。Authentication一般包含了如下信息:
principal: 识别用户。当用用户名/密码进行认证时,这通常是 UserDetails 的一个实例。
credentials: 通常是一个密码。在许多情况下,这在用户被认证后被清除,以确保它不会被泄露。
authorities: GrantedAuthority 实例是用户被授予的权限。
public interface Authentication extends Principal, Serializable {//获取用户权限,一般情况下获取到的是用户的角色信息Collection<? extends GrantedAuthority> getAuthorities();//获取证明用户认证的信息,通常情况下获取到的是密码等信息Object getCredentials();//获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)Object getDetails();// 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetailsObject getPrincipal();//获取当前 Authentication 是否已认证boolean isAuthenticated();//设置当前 Authentication 是否已认证(true or false)void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
1.1.4、GrantedAuthority
Spring Security定义的权限类规范接口,认证后的用户权限就是以GrantedAuthority类型的集合保存的。使用时分两种权限,分别是角色(role)和作用域(scope)。role类型的权限表示该权限为角色,角色可能会对应许多的具体资源(菜单、接口等)权限;scope表示某个具体资源的权限。一般使用role类型的权限,因为使用scope的话,认证时可能会保存有非常多的GrantedAuthority,容易导致内存不足,而role类型基本没有这种问题。注意设置role类型的权限时,权限最好加上ROLE_ 前缀,Spring Security默认的role类型鉴权方法会有ROLE_ 前缀。
public interface GrantedAuthority extends Serializable {//拿到权限名String getAuthority();
}
1.1.5、AuthenticationManager
认证管理器接口,定义了执行认证逻辑的方法API。常用的实现类为ProviderManager。
public interface AuthenticationManager {//用户执行认证时的方法,具体逻辑由实现类实现Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
1.1.6 、ProviderManager
ProviderManager是最常用的AuthenticationManager的实现。ProviderManager委托给一个 AuthenticationProvider集合。每个 AuthenticationProvider都有机会表明认证应该是成功的、失败的,或者表明它不能做出决定并允许下游的AuthenticationProvider来决定。如果配置的 AuthenticationProvider实例中没有一个能进行认证,那么认证就会以ProviderNotFoundException 而失败,这是一个特殊的AuthenticationException,表明ProviderManager没有被配置为支持被传入它的Authentication类型。
1.1.7、AuthenticationProvider
实际上执行认证逻辑的地方。常用的实习类DaoAuthenticationProvider(支持基于用户名/密码的认证)、AnonymousAuthenticationProvider(匿名用户认证)
public interface AuthenticationProvider {//执行具体认证逻辑Authentication authenticate(Authentication authentication) throws AuthenticationException;boolean supports(Class<?> authentication);
}
1.1.8、AuthenticationEntryPoint
如果用户访问一个需要认证后才能访问的资源,AuthenticationEntryPoint就会返回一个响应,需要用户先认证后或者携带认证凭证再访问。比如重定向到登录页面,或者返回一个携带“需要登录”提示的响应信息。我们可以实现该接口,自定义的未登录认证提示。Spring Security默认会对未认证去访问需要认证的资源的请求返回403。
1.1.9、AbstractAuthenticationProcessingFilter
用户认证的基础Filter,只有UsernamePasswordAuthenticationFilter这一个实现类。
AbstractAuthenticationProcessingFilter源码:
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {...//主要方法public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {this.doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);}private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {//先校验请求url与表单校验提交的url是否一致,不一致执行下一个Filter//一致的话就执行认证逻辑,一般默认的表单提交url是"/login"if (!this.requiresAuthentication(request, response)) {chain.doFilter(request, response);} else {try {//实现类执行具体的认证逻辑Authentication authenticationResult = this.attemptAuthentication(request, response);if (authenticationResult == null) {return;}this.sessionStrategy.onAuthentication(authenticationResult, request, response);if (this.continueChainBeforeSuccessfulAuthentication) {chain.doFilter(request, response);}this.successfulAuthentication(request, response, chain, authenticationResult);} catch (InternalAuthenticationServiceException var5) {this.logger.error("An internal error occurred while trying to authenticate the user.", var5);this.unsuccessfulAuthentication(request, response, var5);} catch (AuthenticationException var6) {this.unsuccessfulAuthentication(request, response, var6);}}}//由子类实现具体的验证逻辑public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException;...
}
UsernamePasswordAuthenticationFilter的 attemptAuthentication() 方法
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 {//取用户名,实际上是从request取username参数String username = this.obtainUsername(request);username = username != null ? username : "";username = username.trim();//取密码,实际上是从request取password参数String password = this.obtainPassword(request);password = password != null ? password : "";UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);this.setDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}}
AbstractAuthenticationProcessingFilter认证步骤:
1、当用户提交他们的凭证(用户名和密码)时,AbstractAuthenticationProcessingFilter会从HttpServletRequest中创建一个要认证的Authentication。创建的认证的类型取决于 AbstractAuthenticationProcessingFilter的子类。例如,UsernamePasswordAuthenticationFilter从HttpServletRequest中提交的username和password创建一个 UsernamePasswordAuthenticationToken。
2、接下来,Authentication被传入AuthenticationManager,执行认证逻辑。
3、如果认证失败,则为Failure。
-
SecurityContextHolder被清空。
-
RememberMeServices.loginFail被调用。如果没有配置记住我(remember me),可以忽略。
-
AuthenticationFailureHandler被调用。参考AuthenticationFailureHandler接口。
4、 如果认证成功,则为Success。
-
SessionAuthenticationStrategy被通知有新的登录。参考SessionAuthenticationStrategy接口。
-
Authentication是在SecurityContextHolder上设置的。如果你需要保存SecurityContext以便在未来的请求中自动设置,必须显式调用SecurityContextRepository#saveContext。参考 SecurityContextHolderFilter类。
-
RememberMeServices.loginSuccess 被调用。如果没有配置remember me,可以忽略。
-
ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent事件。
-
AuthenticationSuccessHandler被调用。参考AuthenticationSuccessHandler接口。
1.2、代码示例
1.2.1、默认登录认证
引入需要用到的相关包。
<dependencies><!-- 如果你项目的maven父工程是spring-boot-starter-parent包,可以不写版本号,springboot管理了版本号--><!--Spring Security--><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><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></exclusion></exclusions></dependency><!--使用undertow容器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-undertow</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency></dependencies>
定义一个controller
@Controller
public class LoginController {//主页url@RequestMapping("/main")public String mainPage(){return "main";}
}
在resource/templates/ 路径下里定义一个main.html作为主页
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>主页面</title>
</head>
<body><h1>主页面</h1><form th:action="@{/logout}" method="post"><input type="submit" value="登出"></form>
</body>
</html>
application.yml
server:port: 8084servlet:context-path: /securityspring:security:#配置Spring Security默认登录用户和密码#不配置的话,启动项目时,Spring Security会在控制台打印出默认密码,用户名是Useruser:name: Userpassword: 123456
一切准备就绪,启动项目,访问localhost:8084/security/main,会自动重定向到Spring Security的默认登录页面。
这是因为Spring Security使用了默认的表单登录认证的方式。查看控制台打印信息,可以看到类似下面的输出。
如果没有,可能是Spring Security的版本问题,我使用的Spring Boot-2.6.2引入的Spring Security-5.6.2,关于这一块的打印信息逻辑写错了,导致未打印,可以将Spring Boot版本升级一下。
2023-06-14T08:55:22.321-03:00 INFO 76975 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]
UsernamePasswordAuthenticationFilter过滤器就是用来表单登录认证的Filter。
1.2.2、自定义登录页面
在resource/templates/ 路径下里定义一个login.html作为登录页
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>登录页面</title>
</head>
<body><h1>登录页面</h1><!--method必须为post--><!--th:action="@{/login}",使用动态参数,表单中会自动生成_csrf隐藏字段,用于防止csrf攻击login:和登录页面保持一致即可,SpringSecurity自动进行登录认证/login 是Spring Security默认的登录认证路径,默认情况下用户名和密码名称必须是username和password--><form th:action="@{/login}" method="post">用户名:<input type="text" name="username"> <br>密码:<input type="password" name="password"><br><input type="submit"></form>
</body>
</html>
LoginController添加登录页面跳转接口
@RequestMapping("/myLoginPage")public String myLoginPage(){return "login";}
自定义Spring Security的配置类
@Configuration
public class BasicSecurityConfig {@Beanpublic SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {// 登录相关配置http.formLogin(formLogin -> formLogin.loginPage("/myLoginPage") // 自定义登录页面,不再使用内置的自动生成页面//登录认证接口url,这里可以任意设置,只要保证和登录表单提交的url相同即可.loginProcessingUrl("/login").usernameParameter("username")// 表单中的用户名项.passwordParameter("password")// 表单中的密码项.successForwardUrl("/main")//登录成功后跳转的路径,未设置会跳转到项目根路径);//设置访问权限,如果不设置,默认所有的url都可以匿名访问http.authorizeRequests(authorize ->{authorize.antMatchers("/myLoginPage").permitAll() //允许所有用户访问.anyRequest() //对所有请求开启授权保护.authenticated(); //已认证的请求会被自动授权});http.logout(logout ->logout.logoutUrl("/logout") //使用该方法时,当开启csrf防护,logout请求必须是post,否则会404.clearAuthentication(true) //清除认证状态,默认为true.invalidateHttpSession(true) // 销毁HttpSession对象,默认为true);//关闭csrf防护,否则所有的POST的请求都需要携带CSRF令牌http.csrf(csrf -> csrf.disable());return http.build(); // 返回构建的SecurityFilterChain实例}
}
还有一种写法是继承WebSecurityConfigurerAdapter类,重写configure方法,但是Spring Security 6.0及之后的版本删除了WebSecurityConfigurerAdapter类,不能用这种写法配置了。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Overridepublic void configure(HttpSecurity http) throws Exception {//和上面配置相同,最后无需调用http.build()方法....}
}
注意:使用表单登录认证时,实际处理认证的是UsernamePasswordAuthenticationFilter类,loginProcessingUrl方法配置的url可以任意配置,只要和登录表单提交的url相同即可。
1.2.3、自定义Handler逻辑
Spring Security定义了一些Handler接口,让我们可以自定义认证结束后的处理逻辑。比如返回JSON结果,适用于前后端分离的项目。
1.2.3.1、认证成功处理
AuthenticationSuccessHandler类是Spring Security提供的认证成功后处理逻辑接口。
实现AuthenticationSuccessHandler接口:
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {// 获取用户身份信息UserDetails userDetails = (UserDetails)authentication.getPrincipal();// 获取用户的凭证信息Object credentials = authentication.getCredentials();// 获取用户权限信息Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();/*返回页面,适用于前后端未分离的项目*/System.out.println("用户名:"+userDetails.getUsername());System.out.println("一些操作...");//response.sendRedirect(request.getContextPath()+"/main");/*返回json,适用于前后端分离*///这里可以生成token,并存redis等Map<String,Object> result = new HashMap();result.put("code",0); // 成功result.put("message","登录成功"); //result.put("data",userDetails); //这里可以换成token,jwt等登录成功凭证// 将结果对象转换成json字符串String json = JSON.toJSONString(result);// 返回json数据到前端// 响应头response.setContentType("application/json;charset=UTF-8");// 响应体response.getWriter().println(json);}
}
在BasicSecurityConfig配置类的formLogin中加上MyAuthenticationSuccessHandler
// 登录相关配置http.formLogin(formLogin -> formLogin.loginPage("/myLoginPage") // 自定义登录页面,不再使用内置的自动生成页面//登录认证接口url,这里可以任意设置,只要保证和登录表单提交的url相同即可.loginProcessingUrl("/login").usernameParameter("username")// 表单中的用户名项.passwordParameter("password")// 表单中的密码项.successForwardUrl("/main")//登录成功后跳转的路径,未设置会跳转到项目根路径.successHandler(new MyAuthenticationSuccessHandler()) //认证成功处理);
1.2.3.2、认证失败处理
AuthenticationFailureHandler类是Spring Security提供的认证失败处理逻辑接口。
实现AuthenticationFailureHandler接口:
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {// 获取失败的信息String localizedMessage = exception.getLocalizedMessage();Map<String,Object> result = new HashMap();result.put("code",-1); // 失败result.put("message",localizedMessage); //// 将结果对象转换成json字符串String json = JSON.toJSONString(result);// 返回json数据到前端// 响应头response.setContentType("application/json;charset=UTF-8");// 响应体response.getWriter().println(json);//重定向到登录错误页面,适用与前后端不分离项目//response.sendRedirect(request.getContextPath()+"/loginError");}
}
在BasicSecurityConfig配置类的formLogin中加上MyAuthenticationFailureHandler
// 登录相关配置http.formLogin(formLogin -> formLogin....successHandler(new MyAuthenticationSuccessHandler()) //认证成功处理.failureHandler(new MyAuthenticationFailureHandler()) //认证失败处理);
1.2.3.3、登出成功处理
LogoutSuccessHandler类是Spring Security提供的登出成功处理逻辑接口。
实现LogoutSuccessHandler接口:
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {// 获取用户身份信息UserDetails userDetails = (UserDetails)authentication.getPrincipal();/** 返回json,适用于前后端分离*/Map<String,Object> result = new HashMap();result.put("code",1); // 成功result.put("message","注销成功"); //result.put("data",userDetails); //// 将结果对象转换成json字符串String json = JSON.toJSONString(result);// 返回json数据到前端 适用前后端分离// 响应头response.setContentType("application/json;charset=UTF-8");// 响应体response.getWriter().println(json);//返回到页面//response.sendRedirect(request.getContextPath()+"/main");}
}
在BasicSecurityConfig配置类的logout中加上MyLogoutSuccessHandler
http.logout(logout ->logout.logoutUrl("/logout") //使用该方法时,当开启csrf防护,logout请求必须是post,否则会404.clearAuthentication(true) //清除认证状态,默认为true.invalidateHttpSession(true) // 销毁HttpSession对象,默认为true.logoutSuccessHandler(new MyLogoutSuccessHandler()));
1.2.3.4、请求未认证资源处理
AuthenticationEntryPoint类是Spring Security提供的未认证访问资源处理逻辑接口。
实现AuthenticationEntryPoint类:
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {String localizedMessage = "需要登录";//authException.getLocalizedMessage();Map<String,Object> result = new HashMap();result.put("code",-1); // 告诉用户需要登录result.put("message",localizedMessage); //// 将结果对象转换成json字符串String json = JSON.toJSONString(result);// 返回json数据到前端// 响应头response.setContentType("application/json;charset=UTF-8");// 响应体response.getWriter().println(json);//返回登录界面//response.sendRedirect(request.getContextPath()+"/myLoginPage");}
}
在BasicSecurityConfig配置类的中加上配置
//异常处理http.exceptionHandling(exception -> exception.authenticationEntryPoint(new MyAuthenticationEntryPoint()) //请求未认证的处理);
1.2.4、基于数据库的认证
前面的示例中,我们的登录用户是写在配置文件里的,用的是基于内存存储用户信息的方式。这只能在学习时使用,在实际项目中是不行的。实际项目中,我们的用户信息时存在数据库里的,Spring Security也提供了基于数据库来进行认证的方式。
前文我们已经说过,通过HttpSecurity的formLogin方法配置的认证,是使用UsernamePasswordAuthenticationFilter类来进行的认证处理,而实际上处理时,是在ProviderManager的authenticate方法里,再调用DaoAuthenticationProvider的authenticate方法处理的。最终的处理是在DaoAuthenticationProvider类的父类AbstractUserDetailsAuthenticationProvider类的authenticate处理的。
而在进行认证前,需要先根据用户名查询系统里的用户数据(内存或数据库),再根据查询到的用户密码与用户输入的密码校验,校验通过,则认证成功。这一块的逻辑是由DaoAuthenticationProvider类重写父类的retrieveUser实现的。源码如下:
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);}}
通过调用UserDetailsService的loadUserByUsername方法,返回系统的用户信息。我们可以通过实现自己的UserDetailsService实现类,重写loadUserByUsername方法,查询数据库里的用户数据。代码如下:
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {/*** UserDetails提供的字段如果不够的话,可以继承 User类,实现自己的UserDetails* 用户认证时会调用* @param username* @return* @throws UsernameNotFoundException*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//根据userName去数据库查询用户信息, 伪代码UserDomain user = userService.queryUserByUserName(username);if(user == null){throw new UsernameNotFoundException(username);}//查询用户的角色,伪代码List<String> roles = roleService.getRoleCodeByUserId(user.getId());UserDetails userDetails = User.withUsername(user.getLoginName()).password(user.getPassword())//.authorities(roles.toArray(new String[roles.size()])) //权限,和roles配一个就行,这里配置不会加前缀.roles(roles.toArray(new String[roles.size()])) //角色 配置角色时,会给资源自动加上ROLE_前缀.build();return userDetails;}@Overridepublic void createUser(UserDetails user) {}@Overridepublic void updateUser(UserDetails user) {}@Overridepublic void deleteUser(String username) {}@Overridepublic void changePassword(String oldPassword, String newPassword) {}@Overridepublic boolean userExists(String username) {return false;}@Overridepublic UserDetails updatePassword(UserDetails user, String newPassword) {return null;}
}
然后在配置类中加上相关配置:
@Configuration
public class BasicSecurityConfig {.../*** 密码编码器,会对请求传入的密码进行加密* @return*/@Beanpublic PasswordEncoder passwordEncoder() {//return NoOpPasswordEncoder.getInstance();return new BCryptPasswordEncoder();}@Beanpublic UserDetailsService userDetailsService(){return new DBUserDetailsManager();}...
}
需要加一个密码编码器,使用Spring Security提供的默认编码器就行,使用编码器后,注意数据库保存的密码应该是密文。直接将我们的UserDetailsService注入到Spring容器中即可生效。
二、Spring Security鉴权
2.1、鉴权架构
2.1.1、FilterSecurityInterceptor
Spring Security进行鉴权处理的入口。父类是AbstractSecurityInterceptor类
2.1.2、AccessDecisionManager
Spring Security鉴权的真正处理者
public interface AccessDecisionManager {//鉴权方法 /*** authentication 当前用户的认证凭证信息,包括了用户信息,权限等* object 一般是FilterInvocation,包含了当前请求的request和response* configAttributes过滤规则,由配置类里的 HttpSecurity的authorizeRequests方法配置*/void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException;//是否允许AccessDecisionManager处理该过滤规则,true为允许boolean supports(ConfigAttribute attribute);//是否允许AccessDecisionManager处理clazz类型,true为允许boolean supports(Class<?> clazz);
}
Spring Security的鉴权是基于投票机制的鉴权方式。
2.1.3、AccessDecisionVoter
投票器,AccessDecisionManager的投票处理是由AccessDecisionVoter投票器决定的,一个AccessDecisionManager里会包含一个AccessDecisionVoter集合,AccessDecisionManager会根据所有投票器的投票结果来决定请求是否有权访问,无权限会抛出一个 AccessDeniedException。
public interface AccessDecisionVoter<S> {//同意int ACCESS_GRANTED = 1;//弃权int ACCESS_ABSTAIN = 0;//反对int ACCESS_DENIED = -1;boolean supports(ConfigAttribute attribute);boolean supports(Class<?> clazz);//投票方法int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
}
AccessDecisionManager有三个实现类
- AffirmativeBased:一票通过,只要有一票通过就算通过,默认是它。
- UnanimousBased:一票反对,只要有一票反对就不能通过。
- ConsensusBased:少数票服从多数票。
2.2、代码示例
2.2.1、默认鉴权
定义两个接口,分别由两种权限访问。在LoginController中新增
//admin权限@RequestMapping("/adminRole")@ResponseBodypublic String adminRole(){return "success";}//tourist权限@RequestMapping("/touristRole")@ResponseBodypublic String touristRole(){return "success";}
在BasicSecurityConfig配置类中新增这两个接口鉴权配置:
@Beanpublic SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {//和前文一样的配置省略了...//设置访问权限,如果不设置,默认所有的url都可以匿名访问http.authorizeRequests(authorize ->{authorize// 放行所有OPTIONS请求.antMatchers(HttpMethod.OPTIONS).permitAll().antMatchers("/myLoginPage").permitAll() //登录页面允许所有用户访问.antMatchers("/adminRole").hasRole("AdminManager") // /adminRole 只能AdminManager角色访问.antMatchers("/touristRole").hasAnyRole("AdminManager","ApproveUser") // /touristRole AdminManager和ApproveUser角色都能访问.anyRequest() //对所有请求开启授权保护.authenticated(); //已认证的请求会被自动授权});...return http.build(); // 返回构建的SecurityFilterChain实例}
通过给“/adminRole”和"/touristRole"接口配置权限过滤规则,用户访问接口时,就会在登录认证成功后,在SecurityContext上下文中设置凭证信息,其中就包括当前用户的权限,然后匹配配置的权限过滤规则,判断当前用户是否有该接口的权限。如果不配置权限过滤规则,则默认认证成功的用户都可以访问。
前文说过,在进行表单登录认证时,Spring Security是通过调用UserDetailsService的loadUserByUsername方法,得到当前登录用户的信息的,其中就包括权限信息。
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//根据userName去数据库查询用户信息, 伪代码UserDomain user = userService.queryUserByUserName(username);if(user == null){throw new UsernameNotFoundException(username);}//查询用户的角色,伪代码List<String> roles = roleService.getRoleCodeByUserId(user.getId());UserDetails userDetails = User.withUsername(user.getLoginName()).password(user.getPassword())//.authorities(roles.toArray(new String[roles.size()])) //权限,和roles配一个就行,这里配置不会加前缀.roles(roles.toArray(new String[roles.size()])) //角色 配置角色时,会给资源自动加上ROLE_前缀.build();return userDetails;}
通过Spring Security的User类的roles和authorities方法,就可以设置当前登录用户的权限信息。这里需要注意的是,如果配置权限过滤规则时,使用的是role(角色)权限,loadUserByUsername方法也得设置role权限,反之亦然。权限名称相同即可。
2.2.2、请求未授权接口处理
Spring Security定义了AccessDeniedHandler接口,用来处理访问未授权接口的请求。只需实现AccessDeniedHandler接口,然后将自定义的类加入到配置里即可。
public class MyAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {Map<String,Object> result = new HashMap();result.put("code",-1); // 没有权限result.put("message","没有权限"); //// 将结果对象转换成json字符串String json = JSON.toJSONString(result);// 返回json数据到前端// 响应头response.setContentType("application/json;charset=UTF-8");// 响应体response.getWriter().println(json);//返回页面//response.sendRedirect(request.getContextPath()+"/main");}
}
在BasicSecurityConfig配置类中加上该类
@Beanpublic SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {...//异常处理http.exceptionHandling(exception -> exception.authenticationEntryPoint(new MyAuthenticationEntryPoint()) //请求未认证的处理.accessDeniedHandler(new MyAccessDeniedHandler()) //未授权处理);//关闭csrf防护,否则所有的POST的请求都需要携带CSRF令牌http.csrf(csrf -> csrf.disable());return http.build(); // 返回构建的SecurityFilterChain实例}
2.2.3、基于方法注解的方式鉴权
Spring Security提供了基于注解的方式,设置权限过滤规则的方法。具体使用如下:
使用@EnableMethodSecurity注解开启基于方法的授权,在自定义的BasicSecurityConfig配置类上加上即可
@Configuration
@EnableMethodSecurity
public class BasicSecurityConfig {...
}
然后在Controller的方法上使用@PreAuthorize注解即可。首先在配置类里去掉"/adminRole"和"/touristRole"的权限过滤规则配置。然后在LoginController里给这两个接口加上@PreAuthorize注解:
@RequestMapping("/adminRole")@ResponseBody@PreAuthorize("hasAnyRole('AdminManager')")public String adminRole(){return "success";}@RequestMapping("/touristRole")@ResponseBody@PreAuthorize("hasAnyRole('AdminManager','ApproveUser')")public String touristRole(){return "success";}
@PreAuthorize里可以使用SpEL表达式,例如:hasRole(‘ADMIN’) and authentication.name == ‘User’ 这种。可以使用的规则如下:
具体可以参考Spring Security关于这一块的官网介绍:
https://springdoc.cn/spring-security/servlet/authorization/authorize-http-requests.html#authorization-expressions
相类似的注解还有@PostAuthorize、@PreFilter、@PostFilter等。具体的用法也可以去官网查找。
相关文章:

Spring Security学习笔记(二)Spring Security认证和鉴权
前言:本系列博客基于Spring Boot 2.6.x依赖的Spring Security5.6.x版本 上一篇博客介绍了Spring Security的整体架构,本篇博客要讲的是Spring Security的认证和鉴权两个重要的机制。 UsernamePasswordAuthenticationFilter和BasicAuthenticationFilter是…...

产品经理NPDP好考吗?
NPDP是新产品开发专业人员的资格认证,对于希望在产品管理领域取得认可的专业人士来说,NPDP认证是一项重要的资格。 那么,产品经理考取NPDP资格认证究竟难不难呢? 首先,NPDP考试的难易程度取决于考生的背景和准备情况…...

【C++】:红黑树的应用 --- 封装map和set
点击跳转至文章:【C】:红黑树深度剖析 — 手撕红黑树! 目录 前言一,红黑树的改造1. 红黑树的主体框架2. 对红黑树节点结构的改造3. 红黑树的迭代器3.1 迭代器类3.2 Begin() 和 End() 四,红黑树相关接口的改造4.1 Find…...

unity美术资源优化(资源冗余,主界面图集过多)
图片资源冗余: UPR unity的性能优化工具检查资源 1.检查纹理读/写标记 开启纹理资源的读/写标志会导致双倍的内存占用 检查Inspector -> Advanced -> Read/Write Enabled选项 2.检查纹理资源alpha通道 如果纹理的alpha通道全部为0,或者全部为2…...

【git】github中的Pull Request是什么
在 Git 中,"pull request"(简称 PR)是一种在分布式版本控制系统中使用的功能,特别是在使用 GitHub、GitLab、Bitbucket 等基于 Git 的代码托管平台时。Pull Request 允许开发者请求将他们的代码更改合并到另一个分支&am…...

gitlab查询分支API显示不全,只有20个问题
背景 gitlab查询分支API需要查询所有分支,且分支数量大于20,但目前调用接口返回的branch最多就显示了20个 解决方案 根据GitLab的文档,查询分支API默认最多返回20个分支。如果要一次性显示80个分支,可以使用分页参数来获取所有…...

vue3+vite 实现动态引入某个文件夹下的组件 - glob-import的使用
<template><div class"user-content"><HeaderTitle title"用户详情"></HeaderTitle><div class"main-content"><div><UserForm /></div><div><TableList></TableList></d…...

hhhhh
x torch.tensor([1.0,0.],[-1.,1.],requires_gradTrue) z x.pow(2).sum() z.backward() x.grad在这段代码中,我们利用 PyTorch 进行自动求梯度,下面详细解释代码的每一个部分及其在反向传播中的作用。同时,我们也将介绍函数对象和叶子节点的…...

扫雷小游戏纯后端版
package com.wind;import java.util.Random; import java.util.Scanner;public class ResultLei {static Random random new Random();public static void main(String[] args) {boolean end true;while (end) {System.out.println("请输入你选择的难度对应的数字&#…...

RuoYi-Vue-Plus(动态添加移除数据源)
一、添加数据 private final DynamicRoutingDataSource dynamicRoutingDataSource;private final DefaultDataSourceCreator dataSourceCreator;//添加一个dynamic的数据源@GetMapping("createDynamic")public void createDynamic() {DataSourceProperty property =…...

idea启动项目报:the command line via JAR manifest or via a classpath file and rerun.
解决方案 1.打开Edit Configurations,进去编辑,如下: 笔记配置 2.选择Modfiy options,点击Shorten command line 3.在新增的Shorten command line选项中选择JAR manifest或classpath file 4.点击保存后即可...

vue3 + ts中有哪些类型是由vue3提供的?
在 Vue 3 中结合 TypeScript 使用时,Vue 提供了一系列的类型帮助函数和接口,这些类型用于增强 TypeScript 的集成和提供类型安全。以下是一些由 Vue 3 提供的常用 TypeScript 类型: RefType: 用于标注一个 ref 返回的响应式引用类型。Reacti…...

【Linux】远程连接Linux虚拟机(MobaXterm)
【Linux】远程连接Linux虚拟机(MobaXterm) 零、原因 有时候我们在虚拟机中操作Linux不太方便,比如不能复制粘贴,不能传文件等等,我们在主机上使用远程连接软件远程连接Linux虚拟机后可以解决上面的问题。 壹、软件下…...

LeetCode Hot100 生成特殊数字的最少操作
给你一个下标从 0 开始的字符串 num ,表示一个非负整数。 在一次操作中,您可以选择 num 的任意一位数字并将其删除。请注意,如果你删除 num 中的所有数字,则 num 变为 0。 返回最少需要多少次操作可以使 num 变成特殊数字。 如…...

Spring MVC 应用分层
1. 类名使⽤⼤驼峰⻛格,但以下情形例外:DO/BO/DTO/VO/AO 2. ⽅法名、参数名、成员变量、局部变量统⼀使⽤⼩驼峰⻛格 3. 包名统⼀使⽤⼩写,点分隔符之间有且仅有⼀个⾃然语义的英语单词. 常⻅命名命名⻛格介绍 ⼤驼峰: 所有单词⾸字⺟…...

QT--进程
一、进程QProcess QProcess 用于启动和控制外部进程,管理其输入输出流。 使用方法 start():启动一个新进程。setStandardInputFile():将文件作为标准输入。将进程的标准输入(stdin)重定向到指定的文件。换句话说&am…...

凸优化笔记-基本概念
原文 文章目录 最小二乘问题 仿射affine hullaffine dimension 凸集锥集超平面和半空间单纯形整半定锥保凸性的操作透视函数 凸函数的条件1阶判定条件2阶判定条件 Epigraph 外图 m i n i m i z e f 0 ( x ) minimize\ \ \ f_0(x) minimize f0(x) s u b j e c t t o f i ( …...

1858. 数组查找及替换
问题描述 给定某整数数组和某一整数 b 。 要求删除数组中可以被 b 整除的所有元素,同时将该数组各元素按从小到大排序。如果数组元素数值在 𝐴‘ 到 Z 的 ASCII 之间,替换为对应字母。 元素个数不超过 100,𝑏 在 1 …...

计算机视觉与面部识别:技术、应用与未来发展
引言 在当今数字化时代,计算机视觉技术迅速发展,成为人工智能领域的一个重要分支。计算机视觉旨在让机器理解和解释视觉信息,模拟人类的视觉系统。它在各行各业中发挥着重要作用,从自动驾驶汽车到智能监控系统,再到医疗…...

懒人精灵安卓版纯本地离线文字识别插件
目的 懒人精灵是一款可以模拟鼠标和键盘操作的自动化工具。它可以帮助用户自动完成一些重复的、繁琐的任务,节省大量人工操作的时间。懒人精灵也包含图色功能,识别屏幕上的图像,根据图像的变化自动执行相应的操作。本篇文章主要讲解下更优秀的…...

在线教育数仓项目(数据采集部分1)
文章目录 数据仓库概念项目需求及架构设计项目需求分析系统数据流程设计框架版本选型集群规模估算集群资源规划设计 数据生成模块目标数据页面事件曝光启动播放错误 数据埋点主流埋点方式(了解)埋点数据上报时机埋点数据日志结构 服务器和JDK准备服务器准…...

帕金森病(PD)诊断:三种基于语音的深度学习方法
帕金森病(Parkinson’s disease, PD)是世界上第二大流行的神经退行性疾病,全球影响着超过1000万人,仅次于阿尔茨海默症。人们通常在65岁左右被诊断出患有此病。PD的一些症状包括震颤、肌肉僵硬和运动迟缓。这些症状往往出现在较晚…...

【资料分享】2024钉钉杯大数据挑战赛A题思路解析+代码演示
2024第三届钉钉杯大学生大数据挑战赛今天已经开赛,【A题】思路解析代码,资料预览:...

【优质精选】12节大模型系列教学课程之二:RAG 原理与应用
课程二:RAG 原理与应用 12节大模型系列教学课程之二:RAG 原理与应用 课程详细内容RAG 技术的基础知识RAG 的工作原理RAG 提高生成质量和准确性的原理RAG 在问答系统中的应用RAG 在文本创作中的应用RAG 在其他领域的应用探索RAG 技术的挑战与应对策略RAG …...

vue3前端开发-小兔鲜项目-产品详情基础数据渲染
vue3前端开发-小兔鲜项目-产品详情基础数据渲染!这一次内容比较多,我们分开写。第一步先完成详情页面的基础数据的渲染。然后再去做一下右侧的热门产品的列表内容。 第一步,还是老规矩,先准备好接口函数。方便我们的页面组件拿到对…...

Docker入门指南:Linux系统下的完整安装步骤与常见问题解答
本文以centos7演示。 Docker安装 可参考官方安装文档:Install Docker Engine on CentOS | Docker Docs 一图流: # 移除旧版本docker sudo yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-logro…...

Netty实现数据上下行
Netty实现数据上下行 使用LVSNGinxNetty实现数据接入 在数据上行的时候,通过使用车辆唯一标识码(vin)和连接通道绑定 Netty一些配置参数如下: #netty项目使用的端口 server.port8017 #使用启用epoll(在Linux上拥有更好的传输性…...

【React】事件绑定:深入解析高效处理用户交互的最佳实践
文章目录 一、什么是事件绑定?二、基本事件绑定三、绑定 this 上下文四、传递参数五、事件对象六、事件委托七、常见事件处理八、优化事件处理 React 是现代前端开发中最受欢迎的框架之一,其组件化和高效的状态管理能力使得构建复杂的用户界面变得更加容…...

SpringCloud:使用OpenFeign优化前面的Nacos实现高效购物车商品信息处理
在现代电商系统中,购物车的性能直接影响用户的购物体验。为了提升系统性能和用户满意度,我们可以使用Spring Cloud的OpenFeign和负载均衡器来高效地处理购物车中的商品信息。本文将详细介绍如何在Spring Cloud中集成这些组件,并实现一个高效的…...

计算机三级嵌入式笔记(二)——嵌入式处理器
目录 考点1 嵌入式处理器的结构类型 考点2 嵌入式处理器简介 考点3 ARM处理器概述 考点4 处理器和处理器核 考点5 ARM 处理器的分类 考点6 经典 ARM 处理器 考点7 ARM Cortex 嵌入式处理器 考点8 ARM Cortex实时嵌入式处理器 考点9 ARM Cortex 应用处理器 考点10 AR…...