前言:本系列博客基于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认证信息。
publicinterfaceSecurityContextextendsSerializable{// 获取Authentication对象AuthenticationgetAuthentication();// 放入Authentication对象voidsetAuthentication(Authentication authentication);}
1.1.2、SecurityContextHolder
SecurityContextHolder是用来设置和获取SecurityContext的静态工具类,SecurityContextHolder不关心SecurityContext里认证信息的细节,即Authentication的具体实现类型是什么它并不关心,如果它能获取到值,这个值就认为是当前用户的认证信息。
publicclassSecurityContextHolder{...//常用方法publicstaticvoidclearContext(){
strategy.clearContext();}publicstaticSecurityContextgetContext(){return strategy.getContext();}publicstaticvoidsetContext(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 实例是用户被授予的权限。
publicinterfaceAuthenticationextendsPrincipal,Serializable{//获取用户权限,一般情况下获取到的是用户的角色信息Collection<?extendsGrantedAuthority>getAuthorities();//获取证明用户认证的信息,通常情况下获取到的是密码等信息ObjectgetCredentials();//获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)ObjectgetDetails();// 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetailsObjectgetPrincipal();//获取当前 Authentication 是否已认证booleanisAuthenticated();//设置当前 Authentication 是否已认证(true or false)voidsetAuthenticated(boolean isAuthenticated)throwsIllegalArgumentException;}
1.1.4、GrantedAuthority
Spring Security定义的权限类规范接口,认证后的用户权限就是以GrantedAuthority类型的集合保存的。使用时分两种权限,分别是角色(role)和作用域(scope)。role类型的权限表示该权限为角色,角色可能会对应许多的具体资源(菜单、接口等)权限;scope表示某个具体资源的权限。一般使用role类型的权限,因为使用scope的话,认证时可能会保存有非常多的GrantedAuthority,容易导致内存不足,而role类型基本没有这种问题。注意设置role类型的权限时,权限最好加上ROLE_ 前缀,Spring Security默认的role类型鉴权方法会有ROLE_ 前缀。
publicinterfaceGrantedAuthorityextendsSerializable{//拿到权限名StringgetAuthority();}
1.1.5、AuthenticationManager
认证管理器接口,定义了执行认证逻辑的方法API。常用的实现类为ProviderManager。
publicinterfaceAuthenticationManager{//用户执行认证时的方法,具体逻辑由实现类实现Authenticationauthenticate(Authentication authentication)throwsAuthenticationException;}
1.1.6 、ProviderManager
ProviderManager是最常用的AuthenticationManager的实现。ProviderManager委托给一个 AuthenticationProvider集合。每个 AuthenticationProvider都有机会表明认证应该是成功的、失败的,或者表明它不能做出决定并允许下游的AuthenticationProvider来决定。如果配置的 AuthenticationProvider实例中没有一个能进行认证,那么认证就会以ProviderNotFoundException 而失败,这是一个特殊的AuthenticationException,表明ProviderManager没有被配置为支持被传入它的Authentication类型。
1.1.7、AuthenticationProvider
实际上执行认证逻辑的地方。常用的实习类DaoAuthenticationProvider(支持基于用户名/密码的认证)、AnonymousAuthenticationProvider(匿名用户认证)
publicinterfaceAuthenticationProvider{//执行具体认证逻辑Authenticationauthenticate(Authentication authentication)throwsAuthenticationException;booleansupports(Class<?> authentication);}
1.1.8、AuthenticationEntryPoint
如果用户访问一个需要认证后才能访问的资源,AuthenticationEntryPoint就会返回一个响应,需要用户先认证后或者携带认证凭证再访问。比如重定向到登录页面,或者返回一个携带“需要登录”提示的响应信息。我们可以实现该接口,自定义的未登录认证提示。Spring Security默认会对未认证去访问需要认证的资源的请求返回403。
1.1.9、AbstractAuthenticationProcessingFilter
用户认证的基础Filter,只有UsernamePasswordAuthenticationFilter这一个实现类。
AbstractAuthenticationProcessingFilter源码:
publicabstractclassAbstractAuthenticationProcessingFilterextendsGenericFilterBeanimplementsApplicationEventPublisherAware,MessageSourceAware{...//主要方法publicvoiddoFilter(ServletRequest request,ServletResponse response,FilterChain chain)throwsIOException,ServletException{this.doFilter((HttpServletRequest)request,(HttpServletResponse)response, chain);}privatevoiddoFilter(HttpServletRequest request,HttpServletResponse response,FilterChain chain)throwsIOException,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);}}}//由子类实现具体的验证逻辑publicabstractAuthenticationattemptAuthentication(HttpServletRequest request,HttpServletResponse response)throwsAuthenticationException,IOException,ServletException;...}
UsernamePasswordAuthenticationFilter的 attemptAuthentication() 方法
publicAuthenticationattemptAuthentication(HttpServletRequest request,HttpServletResponse response)throwsAuthenticationException{if(this.postOnly &&!request.getMethod().equals("POST")){thrownewAuthenticationServiceException("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 =newUsernamePasswordAuthenticationToken(username, password);this.setDetails(request, authRequest);returnthis.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
@ControllerpublicclassLoginController{//主页url@RequestMapping("/main")publicStringmainPage(){return"main";}}
在resource/templates/ 路径下里定义一个main.html作为主页
<!DOCTYPEhtml><htmllang="en"xmlns:th="http://www.thymeleaf.org"><head><metacharset="UTF-8"><title>主页面</title></head><body><h1>主页面</h1><formth:action="@{/logout}"method="post"><inputtype="submit"value="登出"></form></body></html>
application.yml
server:port:8084servlet:context-path: /security
spring:security:#配置Spring Security默认登录用户和密码#不配置的话,启动项目时,Spring Security会在控制台打印出默认密码,用户名是Useruser:name: User
password: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作为登录页
<!DOCTYPEhtml><htmllang="en"xmlns:th="http://www.thymeleaf.org"><head><metacharset="UTF-8"><title>登录页面</title></head><body><h1>登录页面</h1><!--method必须为post--><!--th:action="@{/login}",
使用动态参数,表单中会自动生成_csrf隐藏字段,用于防止csrf攻击
login:和登录页面保持一致即可,SpringSecurity自动进行登录认证
/login 是Spring Security默认的登录认证路径,默认情况下用户名和密码名称必须是username和password
--><formth:action="@{/login}"method="post">
用户名:<inputtype="text"name="username"><br>
密码:<inputtype="password"name="password"><br><inputtype="submit"></form></body></html>
LoginController添加登录页面跳转接口
@RequestMapping("/myLoginPage")publicStringmyLoginPage(){return"login";}
自定义Spring Security的配置类
@ConfigurationpublicclassBasicSecurityConfig{@BeanpublicSecurityFilterChainmySecurityFilterChain(HttpSecurity http)throwsException{// 登录相关配置
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类,不能用这种写法配置了。
@ConfigurationpublicclassWebSecurityConfigextendsWebSecurityConfigurerAdapter{@Overridepublicvoidconfigure(HttpSecurity http)throwsException{//和上面配置相同,最后无需调用http.build()方法....}}
注意:使用表单登录认证时,实际处理认证的是UsernamePasswordAuthenticationFilter类,loginProcessingUrl方法配置的url可以任意配置,只要和登录表单提交的url相同即可。
1.2.3、自定义Handler逻辑
Spring Security定义了一些Handler接口,让我们可以自定义认证结束后的处理逻辑。比如返回JSON结果,适用于前后端分离的项目。
1.2.3.1、认证成功处理
AuthenticationSuccessHandler类是Spring Security提供的认证成功后处理逻辑接口。
实现AuthenticationSuccessHandler接口:
publicclassMyAuthenticationSuccessHandlerimplementsAuthenticationSuccessHandler{@OverridepublicvoidonAuthenticationSuccess(HttpServletRequest request,HttpServletResponse response,Authentication authentication)throwsIOException,ServletException{// 获取用户身份信息UserDetails userDetails =(UserDetails)authentication.getPrincipal();// 获取用户的凭证信息Object credentials = authentication.getCredentials();// 获取用户权限信息Collection<?extendsGrantedAuthority> authorities = authentication.getAuthorities();/*返回页面,适用于前后端未分离的项目*/System.out.println("用户名:"+userDetails.getUsername());System.out.println("一些操作...");//response.sendRedirect(request.getContextPath()+"/main");/*返回json,适用于前后端分离*///这里可以生成token,并存redis等Map<String,Object> result =newHashMap();
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(newMyAuthenticationSuccessHandler())//认证成功处理);
1.2.3.2、认证失败处理
AuthenticationFailureHandler类是Spring Security提供的认证失败处理逻辑接口。
实现AuthenticationFailureHandler接口:
publicclassMyAuthenticationFailureHandlerimplementsAuthenticationFailureHandler{@OverridepublicvoidonAuthenticationFailure(HttpServletRequest request,HttpServletResponse response,AuthenticationException exception)throwsIOException,ServletException{// 获取失败的信息String localizedMessage = exception.getLocalizedMessage();Map<String,Object> result =newHashMap();
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(newMyAuthenticationSuccessHandler())//认证成功处理.failureHandler(newMyAuthenticationFailureHandler())//认证失败处理);
1.2.3.3、登出成功处理
LogoutSuccessHandler类是Spring Security提供的登出成功处理逻辑接口。
实现LogoutSuccessHandler接口:
publicclassMyLogoutSuccessHandlerimplementsLogoutSuccessHandler{@OverridepublicvoidonLogoutSuccess(HttpServletRequest request,HttpServletResponse response,Authentication authentication)throwsIOException,ServletException{// 获取用户身份信息UserDetails userDetails =(UserDetails)authentication.getPrincipal();/** 返回json,适用于前后端分离*/Map<String,Object> result =newHashMap();
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(newMyLogoutSuccessHandler()));
1.2.3.4、请求未认证资源处理
AuthenticationEntryPoint类是Spring Security提供的未认证访问资源处理逻辑接口。
实现AuthenticationEntryPoint类:
publicclassMyAuthenticationEntryPointimplementsAuthenticationEntryPoint{@Overridepublicvoidcommence(HttpServletRequest request,HttpServletResponse response,AuthenticationException authException)throwsIOException,ServletException{String localizedMessage ="需要登录";//authException.getLocalizedMessage();Map<String,Object> result =newHashMap();
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(newMyAuthenticationEntryPoint())//请求未认证的处理);
1.2.4、基于数据库的认证
前面的示例中,我们的登录用户是写在配置文件里的,用的是基于内存存储用户信息的方式。这只能在学习时使用,在实际项目中是不行的。实际项目中,我们的用户信息时存在数据库里的,Spring Security也提供了基于数据库来进行认证的方式。
前文我们已经说过,通过HttpSecurity的formLogin方法配置的认证,是使用UsernamePasswordAuthenticationFilter类来进行的认证处理,而实际上处理时,是在ProviderManager的authenticate方法里,再调用DaoAuthenticationProvider的authenticate方法处理的。最终的处理是在DaoAuthenticationProvider类的父类AbstractUserDetailsAuthenticationProvider类的authenticate处理的。
而在进行认证前,需要先根据用户名查询系统里的用户数据(内存或数据库),再根据查询到的用户密码与用户输入的密码校验,校验通过,则认证成功。这一块的逻辑是由DaoAuthenticationProvider类重写父类的retrieveUser实现的。源码如下:
protectedfinalUserDetailsretrieveUser(String username,UsernamePasswordAuthenticationToken authentication)throwsAuthenticationException{this.prepareTimingAttackProtection();try{//拿到用户信息UserDetails loadedUser =this.getUserDetailsService().loadUserByUsername(username);if(loadedUser ==null){thrownewInternalAuthenticationServiceException("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){thrownewInternalAuthenticationServiceException(var6.getMessage(), var6);}}
通过调用UserDetailsService的loadUserByUsername方法,返回系统的用户信息。我们可以通过实现自己的UserDetailsService实现类,重写loadUserByUsername方法,查询数据库里的用户数据。代码如下:
publicclassDBUserDetailsManagerimplementsUserDetailsManager,UserDetailsPasswordService{/**
* UserDetails提供的字段如果不够的话,可以继承 User类,实现自己的UserDetails
* 用户认证时会调用
* @param username
* @return
* @throws UsernameNotFoundException
*/@OverridepublicUserDetailsloadUserByUsername(String username)throwsUsernameNotFoundException{//根据userName去数据库查询用户信息, 伪代码UserDomain user = userService.queryUserByUserName(username);if(user ==null){thrownewUsernameNotFoundException(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(newString[roles.size()]))//角色 配置角色时,会给资源自动加上ROLE_前缀.build();return userDetails;}@OverridepublicvoidcreateUser(UserDetails user){}@OverridepublicvoidupdateUser(UserDetails user){}@OverridepublicvoiddeleteUser(String username){}@OverridepublicvoidchangePassword(String oldPassword,String newPassword){}@OverridepublicbooleanuserExists(String username){returnfalse;}@OverridepublicUserDetailsupdatePassword(UserDetails user,String newPassword){returnnull;}}
然后在配置类中加上相关配置:
@ConfigurationpublicclassBasicSecurityConfig{.../**
* 密码编码器,会对请求传入的密码进行加密
* @return
*/@BeanpublicPasswordEncoderpasswordEncoder(){//return NoOpPasswordEncoder.getInstance();returnnewBCryptPasswordEncoder();}@BeanpublicUserDetailsServiceuserDetailsService(){returnnewDBUserDetailsManager();}...}
需要加一个密码编码器,使用Spring Security提供的默认编码器就行,使用编码器后,注意数据库保存的密码应该是密文。直接将我们的UserDetailsService注入到Spring容器中即可生效。
二、Spring Security鉴权
2.1、鉴权架构
2.1.1、FilterSecurityInterceptor
Spring Security进行鉴权处理的入口。父类是AbstractSecurityInterceptor类
2.1.2、AccessDecisionManager
Spring Security鉴权的真正处理者
publicinterfaceAccessDecisionManager{//鉴权方法 /**
* authentication 当前用户的认证凭证信息,包括了用户信息,权限等
* object 一般是FilterInvocation,包含了当前请求的request和response
* configAttributes过滤规则,由配置类里的 HttpSecurity的authorizeRequests方法配置
*/voiddecide(Authentication authentication,Object object,Collection<ConfigAttribute> configAttributes)throwsAccessDeniedException,InsufficientAuthenticationException;//是否允许AccessDecisionManager处理该过滤规则,true为允许booleansupports(ConfigAttribute attribute);//是否允许AccessDecisionManager处理clazz类型,true为允许booleansupports(Class<?> clazz);}
Spring Security的鉴权是基于投票机制的鉴权方式。
2.1.3、AccessDecisionVoter
投票器,AccessDecisionManager的投票处理是由AccessDecisionVoter投票器决定的,一个AccessDecisionManager里会包含一个AccessDecisionVoter集合,AccessDecisionManager会根据所有投票器的投票结果来决定请求是否有权访问,无权限会抛出一个 AccessDeniedException。
publicinterfaceAccessDecisionVoter<S>{//同意intACCESS_GRANTED=1;//弃权intACCESS_ABSTAIN=0;//反对intACCESS_DENIED=-1;booleansupports(ConfigAttribute attribute);booleansupports(Class<?> clazz);//投票方法intvote(Authentication authentication,S object,Collection<ConfigAttribute> attributes);}
AccessDecisionManager有三个实现类
- AffirmativeBased:一票通过,只要有一票通过就算通过,默认是它。
- UnanimousBased:一票反对,只要有一票反对就不能通过。
- ConsensusBased:少数票服从多数票。
2.2、代码示例
2.2.1、默认鉴权
定义两个接口,分别由两种权限访问。在LoginController中新增
//admin权限@RequestMapping("/adminRole")@ResponseBodypublicStringadminRole(){return"success";}//tourist权限@RequestMapping("/touristRole")@ResponseBodypublicStringtouristRole(){return"success";}
在BasicSecurityConfig配置类中新增这两个接口鉴权配置:
@BeanpublicSecurityFilterChainmySecurityFilterChain(HttpSecurity http)throwsException{//和前文一样的配置省略了...//设置访问权限,如果不设置,默认所有的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方法,得到当前登录用户的信息的,其中就包括权限信息。
publicUserDetailsloadUserByUsername(String username)throwsUsernameNotFoundException{//根据userName去数据库查询用户信息, 伪代码UserDomain user = userService.queryUserByUserName(username);if(user ==null){thrownewUsernameNotFoundException(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(newString[roles.size()]))//角色 配置角色时,会给资源自动加上ROLE_前缀.build();return userDetails;}
通过Spring Security的User类的roles和authorities方法,就可以设置当前登录用户的权限信息。这里需要注意的是,如果配置权限过滤规则时,使用的是role(角色)权限,loadUserByUsername方法也得设置role权限,反之亦然。权限名称相同即可。
2.2.2、请求未授权接口处理
Spring Security定义了AccessDeniedHandler接口,用来处理访问未授权接口的请求。只需实现AccessDeniedHandler接口,然后将自定义的类加入到配置里即可。
publicclassMyAccessDeniedHandlerimplementsAccessDeniedHandler{@Overridepublicvoidhandle(HttpServletRequest request,HttpServletResponse response,AccessDeniedException accessDeniedException)throwsIOException,ServletException{Map<String,Object> result =newHashMap();
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配置类中加上该类
@BeanpublicSecurityFilterChainmySecurityFilterChain(HttpSecurity http)throwsException{...//异常处理
http.exceptionHandling(exception -> exception
.authenticationEntryPoint(newMyAuthenticationEntryPoint())//请求未认证的处理.accessDeniedHandler(newMyAccessDeniedHandler())//未授权处理);//关闭csrf防护,否则所有的POST的请求都需要携带CSRF令牌
http.csrf(csrf -> csrf.disable());return http.build();// 返回构建的SecurityFilterChain实例}
2.2.3、基于方法注解的方式鉴权
Spring Security提供了基于注解的方式,设置权限过滤规则的方法。具体使用如下:
使用@EnableMethodSecurity注解开启基于方法的授权,在自定义的BasicSecurityConfig配置类上加上即可
@Configuration@EnableMethodSecuritypublicclassBasicSecurityConfig{...}
然后在Controller的方法上使用@PreAuthorize注解即可。首先在配置类里去掉"/adminRole"和"/touristRole"的权限过滤规则配置。然后在LoginController里给这两个接口加上@PreAuthorize注解:
@RequestMapping("/adminRole")@ResponseBody@PreAuthorize("hasAnyRole('AdminManager')")publicStringadminRole(){return"success";}@RequestMapping("/touristRole")@ResponseBody@PreAuthorize("hasAnyRole('AdminManager','ApproveUser')")publicStringtouristRole(){return"success";}
@PreAuthorize里可以使用SpEL表达式,例如:hasRole(‘ADMIN’) and authentication.name == ‘User’ 这种。可以使用的规则如下:
具体可以参考Spring Security关于这一块的官网介绍:
相类似的注解还有@PostAuthorize、@PreFilter、@PostFilter等。具体的用法也可以去官网查找。
版权归原作者 飞!!!! 所有, 如有侵权,请联系我们删除。