Spring Security详解
第一节 Spring Security 简介
Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。
-
用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
-
用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。
第二节 Spring Security 核心组件
1. Authentication 认证
//这个接口的实现类都是Token,称为票据,UsernamePasswordAuthenticationToken//我们自定义实现就可以叫MobileCodeAuthenticationTokenpublic interface Authentication extends Principal, Serializable { Collection extends GrantedAuthority> getAuthorities(); Object getCredentials(); Object getDetails(); Object getPrincipal(); boolean isAuthenticated(); void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;}
Authentication接口就是用来携带认证信息的。认证信息包括用户身份信息,密码,及权限列表等
2. UsernamePasswordAuthenticationFilter
账号密码认证过滤器,用于认证用户信息,认证方式是由 AuthenticationManager 接口提供。
3. AuthenticationManager
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException;}
认证管理器,是认证相关的核心接口,也是发起认证的出发点。实际业务中可能根据不同的信息进行认证,所以Spring推荐通过实现 AuthenticationManager 接口来自定义自己的认证方式。Spring 提供了一个默认的实现 ProviderManager。
4. ProviderManager
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { //省略其他属性 private List<AuthenticationProvider> providers = Collections.emptyList(); //省略其他内容}
认证提供者管理器,该类中维护了一个认证提供者列表,只要这个列表中的任何一个认证提供者提供的认证方式认证通过,认证就结束。
5. AuthenticationProvider
public interface AuthenticationProvider { Authentication authenticate(Authentication authentication) throws AuthenticationException; boolean supports(Class> authentication);}
认证提供者,这是一个接口,具体如何认证,就看如何实现该接口。Spring Security 提供了 DaoAuthenticationProvider 实现该接口,这个类就是使用数据库中数据进行认证。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { //省略其他属性 private PasswordEncoder passwordEncoder; //密码加密器,主要用于密码加密 private UserDetailsService userDetailsService; //用户详细信息服务,主要用于查询认证用户信息 //省略其他内容}
6. UserDetailsService
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;}
7. UserDetails
public interface UserDetails extends Serializable { Collection extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled();}
用户的详细信息,主要用于登录认证。
8. SecurityContextHolder
SecurityContextHolder 是最基本的对象,它负责存储当前 SecurityContext 信息。SecurityContextHolder默认使用 ThreadLocal 来存储认证信息,意味着这是一种与线程绑定的策略。在Web场景下的使用Spring Security,在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。
9. SecurityContext
SecurityContext 负责存储认证通过的用户信息(Authentication对象),保存着当前用户是什么,是否已经通过认证,拥有哪些权限等等。
10. AuthenticationSuccessHandler
AuthenticationSuccessHandler 主要用于认证成功后的处理,比如返回页面或者数据。
11. AuthenticationFailureHandler
AuthenticationFailureHandler 主要用于认证失败后的处理,比如返回页面或者数据。
12. AccessDecisionManager
AccessDecisionManager 主要用于实现权限,决定请求是否具有访问的权限。
13. AccessDeniedHandler
AccessDeniedHandler 主要用于无权访问时的处理
第三节 Spring Security 工作流程
1. DelegatingFilterProxy
public class DelegatingFilterProxy extends GenericFilterBean {}public abstract class GenericFilterBean implements Filter, BeanNameAware, EnvironmentAware, EnvironmentCapable, ServletContextAware, InitializingBean, DisposableBean {}
由上面的类定义可以看出,DelegatingFilterProxy
是一个 Filter
,同时,也是一个InitializingBean
。
public interface InitializingBean { void afterPropertiesSet() throws Exception;}
实现了InitializingBean
接口的类,在创建对象并完成属性设置后,会被纳入Spring IOC 容器管理。如果DelegatingFilterProxy
在web.xml
中配置,那么,在容器启动时就会实例化该Filter
,然后完成初始化,随后被纳入 Spring IOC 容器管理。这样就相当于与 Spring 完成整合。
而 DelegatingFilterProxy
由 spring web 提供,与 Spring Security 无关。那么 DelegatingFilterProxy
到底有什么作用呢?
其作用是代理真正的Filter实现类
DelegatingFilterProxy
如何知道其所代理的Filter是哪个呢?
这是通过其自身的targetBeanName的属性来确定的,通过该名称,DelegatingFilterProxy
可以从WebApplicationContext
中获取指定的 bean
作为代理对象。该属性可以通过在web.xml
中定义 DelegatingFilterProxy
时通过 init-param
来指定,如果未指定,则将取其在web.xml
中声明时定义的名称作为 targetBeanName 的值。
<filter> <filter-name>springSecurityFilterChainfilter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class> <init-param> <param-name>targetBeanNameparam-name> <param-value>springSecurityFilterChainparam-value> init-param>filter><filter-mapping> <filter-name>springSecurityFilterChainfilter-name> <url-pattern> public SmsAuthenticationToken(Object principal, Object credentials){ super(null); this.principal = principal; this.credentials = credentials; super.setAuthenticated(false); } public SmsAuthenticationToken(Object principal, Object credentials, Collection extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); } @Override public Object getCredentials() { return credentials; } @Override public Object getPrincipal() { return principal; } //这个方法是security框架执行认证流程时调用的,用户不应该调用,应该使用构造方法完成认证 @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); super.setAuthenticated(false); } @Override public void eraseCredentials() { super.eraseCredentials(); this.credentials = null; }}
2.3 创建短信实体
package com.qf.authentication.sms;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;@Data@AllArgsConstructor@NoArgsConstructorpublic class SmsCode { //手机号 private String mobile; //验证码 private String code; //过期时间 private long expire;}
2.4 短信模拟
package com.qf.authentication.controller;import com.qf.authentication.sms.SmsCode;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import javax.imageio.ImageIO;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;import java.awt.*;import java.awt.image.BufferedImage;import java.io.IOException;import java.util.Random;@Controllerpublic class SmsController { @GetMapping("/code") public void imgCode(@RequestParam("mobile")String mobile, HttpSession session, HttpServletResponse response) throws IOException { BufferedImage bi = new BufferedImage(200, 40, BufferedImage.TYPE_INT_RGB); Graphics graphics = bi.getGraphics(); graphics.setColor(Color.GRAY); graphics.fillRect(0, 0, 200, 40); StringBuilder builder = new StringBuilder(); Random r = new Random(); for(int i=0; i<6; i++){ int num = r.nextInt(10); builder.append(num); graphics.setColor(Color.red); graphics.drawString(Integer.toString(num), i*10 + 20, 15); } //创建短信实体 SmsCode code = new SmsCode(mobile, builder.toString(), System.currentTimeMillis() + 5 * 60 * 1000); //将短信实体放入session中 session.setAttribute("smsCode", code); graphics.dispose(); ImageIO.write(bi, "jpg", response.getOutputStream()); }}
2.5 完善短信认证过滤器
//尝试认证@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { if(!request.getMethod().equalsIgnoreCase("POST")){ throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String code = obtainCode(request);//获取短信验证码 //从session中获取发送的短信验证码信息 SmsCode smsCode = (SmsCode) request.getSession().getAttribute("smsCode"); if(code == null || smsCode == null){ throw new AuthenticationServiceException("SMS code cannot be null"); } String mobile = obtainMobile(request);//获取手机号 long currentTime = System.currentTimeMillis();//获取系统当前时间 if(smsCode.getExpire() < currentTime || !mobile.equals(smsCode.getMobile())){//如果系统当前时间比验证码过期时间还要大,说明验证码过期,手机号码与验证码不匹配 throw new AuthenticationServiceException("SMS code is invalid:" + smsCode.getCode()); } else if(!code.equals(smsCode.getCode())){ throw new AuthenticationServiceException("SMS code error"); } SmsAuthenticationToken token = new SmsAuthenticationToken(mobile, code);//创建SMS token this.setDetails(request, token); return this.getAuthenticationManager().authenticate(token);//调用认证管理器认证token}//将请求信息放入token中protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));}
2.6 创建短信认证提供器
package com.qf.authentication.sms;import org.springframework.security.authentication.AuthenticationProvider;import org.springframework.security.core.Authentication;import org.springframework.security.core.AuthenticationException;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;public class SmsAuthenticationProvider implements AuthenticationProvider { //获取认证用户的信息的服务接口 private UserDetailsService userDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String mobile = (String) authentication.getPrincipal(); UserDetails userDetails = userDetailsService.loadUserByUsername(mobile); //通过手机号码获取用户信息 SmsAuthenticationToken authenticationToken = new SmsAuthenticationToken(userDetails, authentication.getCredentials(), userDetails.getAuthorities()); authenticationToken.setDetails(authentication.getDetails()); return authenticationToken; } @Override public boolean supports(Class> authentication) { // 判断 authentication 是不是 SmsCodeAuthenticationToken 类型或者其子类或者其子接口 return SmsAuthenticationToken.class.isAssignableFrom(authentication); } public UserDetailsService getUserDetailsService() { return userDetailsService; } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; }}
2.7 配置短信认证
package com.qf.authentication.config;import com.qf.authentication.handler.LoginFailureHandler;import com.qf.authentication.handler.LoginSuccessHandler;import com.qf.authentication.service.UserService;import com.qf.authentication.sms.SmsAuthenticationFilter;import com.qf.authentication.sms.SmsAuthenticationProvider;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.Bean;import org.springframework.http.HttpMethod;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.authentication.dao.DaoAuthenticationProvider;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.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@EnableWebSecurity //启用securitypublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Autowired private LoginSuccessHandler loginSuccessHandler; @Autowired private LoginFailureHandler loginFailureHandler; @Autowired @Qualifier("authenticationManagerBean") //表示使用指定名称的认证管理器 private AuthenticationManager authenticationManager; //创建密码加密器,并纳入Spring IOC容器管理,该Bean的名字就是方法名 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } //认证管理器配置 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //创建短信认证提供器 SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider(); smsAuthenticationProvider.setUserDetailsService(userService); //将认证提供器添加到认证管理器中 auth.authenticationProvider(smsAuthenticationProvider); //数据库数据认证提供器 DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); //设置认证使用的用户详情服务,业就是查询用户信息的服务 daoAuthenticationProvider.setUserDetailsService(userService); //设置密码使用的加密器 daoAuthenticationProvider.setPasswordEncoder(passwordEncoder()); //设置认证管理构建器使用的认证提供器 auth.authenticationProvider(daoAuthenticationProvider); } //Http认证配置 @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable();//关闭跨站请求模拟 //设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制 http.formLogin().loginPage("/").loginProcessingUrl("/login") .successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll(); //设置获取验证码的请求放行 http.authorizeRequests().antMatchers(HttpMethod.GET, "/code").permitAll() //其他请求需要认证 .anyRequest().authenticated(); //设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制 http.logout().invalidateHttpSession(true).permitAll(); //将短信认证过滤器添加账号密码过滤器的前面 http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public SmsAuthenticationFilter smsAuthenticationFilter() throws Exception { SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter(); //设置短信认证过滤器使用的认证管理器 smsAuthenticationFilter.setAuthenticationManager(authenticationManager); //设置登录成功的处理器 smsAuthenticationFilter.setAuthenticationSuccessHandler(loginSuccessHandler); //设置登录失败的处理器 smsAuthenticationFilter.setAuthenticationFailureHandler(loginFailureHandler); return smsAuthenticationFilter; }}
2.8 修改登录页面
index.html
<html lang="en"><head> <meta charset="UTF-8"> <title>Security登录title>head><body> <form action="login" method="post"> <input type="text" name="username"> <input type="password" name="password"> <input type="submit" value="登录"> form> <form action="sms" method="post" > <input type="text" name="mobile"> <input type="text" name="code"> <input type="button" value="获取验证码" onclick="getCode()"> <input type="submit" value="登录"> form>body><script type="text/javascript"> function getCode(){ let elements = document.getElementsByName("code"); let img = document.createElement("img"); img.class="lazy" data-src = "code?mobile=" + document.getElementsByName("mobile")[0].value; elements[0].after(img); }script>html>
2.9 启动程序进行测试
第四节 Spring Security 授权
1. 启用注解授权
@EnableWebSecurity //启用security//prePostEnabled = true启用@PreAuthorize()//securedEnabled = true启用@Secured()//jsr250Enabled = true启用@RolesAllowed、@PermitAll、@DenyAll 但该注解需要jar包支撑@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter {}
JSR 250 依赖包
<dependency> <groupId>javax.annotationgroupId> <artifactId>jsr250-apiartifactId> <version>1.0version>dependency>
2. HTTP授权配置
//Http认证配置@Overrideprotected void configure(HttpSecurity http) throws Exception { http.csrf().disable();//关闭跨站请求模拟 //设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制 http.formLogin().loginPage("/").loginProcessingUrl("/login") .successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll(); //设置获取验证码的请求放行 http.authorizeRequests().antMatchers(HttpMethod.GET, "/code").permitAll() //授权请求表示任意请求都需要认证才能够访问 .anyRequest().authenticated(); //设置异常处理使用访问拒绝处理器 http.exceptionHandling().accessDeniedHandler(); //设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制 http.logout().invalidateHttpSession(true).permitAll(); //将短信认证过滤器添加账号密码过滤器的前面 http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);}
3. 创建拒绝请求处理器
package com.qf.authentication.handler;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;//HTTP请求被拒绝的处理器@Componentpublic class RequestDeniedHandler implements AccessDeniedHandler { //这里就是拒绝处理的具体步骤实现 @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setContentType("text/html;charset=utf-8"); //返回拒绝处理的信息 response.getWriter().print(accessDeniedException.getMessage()); }}
5. 完善HTTP授权配置
@Autowiredprivate RequestDecisionManager decisionManager;@Autowiredprivate RequestDeniedHandler deniedHandler;//Http认证配置@Overrideprotected void configure(HttpSecurity http) throws Exception { http.csrf().disable();//关闭跨站请求模拟 //设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制 http.formLogin().loginPage("/").loginProcessingUrl("/login") .successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll(); //设置获取验证码的请求放行 http.authorizeRequests().antMatchers(HttpMethod.GET, "/code").permitAll() //授权请求表示任意请求都需要认证才能够访问 .anyRequest().authenticated(); //设置异常处理使用访问拒绝处理器 http.exceptionHandling().accessDeniedHandler(deniedHandler); //设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制 http.logout().invalidateHttpSession(true).permitAll(); //将短信认证过滤器添加账号密码过滤器的前面 http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);}
6. 创建测试请求
package com.qf.authentication.controller;import org.springframework.security.access.annotation.Secured;import org.springframework.security.access.prepost.PreAuthorize;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import javax.annotation.security.RolesAllowed;@RestControllerpublic class TestController { @GetMapping("/test1") @PreAuthorize("hasRole('ROLE_ADMIN')") public String test1(){ return "test1"; } @GetMapping("/test2") @PreAuthorize("hasRole('ROLE_TEST')") public String test2(){ return "test2"; } @GetMapping("/test3") @Secured("ROLE_ADMIN") public String test3(){ return "test3"; } @GetMapping("/test4") @Secured("ROLE_TEST") public String test4(){ return "test4"; } @GetMapping("/test5") @RolesAllowed("ROLE_ADMIN") public String test5(){ return "test5"; } @GetMapping("/test6") @RolesAllowed("ROLE_TEST") public String test6(){ return "test6"; }}
7. 启动程序进行测试
第五节 Security 与 AJAX 对接
这个问题的根本原因在于登录结果的处理和拒绝访问的处理。如果能够判断一个请求是ajax请求,那么问题即将得到解决。
package com.qf.security.util;import javax.servlet.http.HttpServletRequest;//针对请求相关的操作工具类public class RequestUtil { private RequestUtil(){} public static boolean isAjaxRequest(HttpServletRequest request){ String header = request.getHeader("X-Requested-With"); return "XMLHttpRequest".equalsIgnoreCase(header); }}
登录的时候传递的参数在过滤器中获取不到,需要注意:在传递参数的时候要使用get方法传递参数的方式对参数进行拼接,然后赋值给data
$.ajax({ type: 'post', url: 'login', data: "username="+ $("#username").val() + "&password=" + $("#password").val(), success: function (resp) { console.log(resp); }});
来源地址:https://blog.csdn.net/JAVA_KX/article/details/130691948
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341