基础
- spring security的底层就是一个过滤器链
- ExceptionTranslationFilter是一个异常过滤器,用来处理认证授权过程中的异常
- UseranmePasswordAuthenticationFilter,拦截/login的post请求,即认证时校验用户名、密码
- spring boot整合spring security会自动化配置,即引入security依赖即可使用,不需要我们配置过滤器等
- 认证,可理解为登录时的验证,当我们登录时就需要从数据库中查询用户名和密码,使用security只需实现
UserDetailsService
接口,在自定义的实现类中进行查询操作;之后返回一个User对象,这个对象是security为我们提供的,这个对象的属性包括查询到的用户名、密码、权限
- 登录时输入的用户名和密码如何与数据库中的用户名密码比较,我们只需写一个类继承UsernamePasswordAuthenticationFilter,重写attemptAuthentication方法,在当前类中会接收登录时输入的用户名和密码,在attemptAuthentication方法中认证
点击查看源码
| public class UsernamePasswordAuthenticationFilter extends |
| AbstractAuthenticationProcessingFilter { |
| |
| |
| |
| public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username"; |
| public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; |
| |
| private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY; |
| private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY; |
| private boolean postOnly = true; |
| |
| |
| |
| |
| public UsernamePasswordAuthenticationFilter() { |
| super(new AntPathRequestMatcher("/login", "POST")); |
| } |
| |
| |
| |
| |
| public Authentication attemptAuthentication(HttpServletRequest request, |
| HttpServletResponse response) throws AuthenticationException { |
| if (postOnly && !request.getMethod().equals("POST")) { |
| throw new AuthenticationServiceException( |
| "Authentication method not supported: " + request.getMethod()); |
| } |
| |
| String username = obtainUsername(request); |
| String password = obtainPassword(request); |
| |
| if (username == null) { |
| username = ""; |
| } |
| |
| if (password == null) { |
| password = ""; |
| } |
| |
| username = username.trim(); |
| |
| UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( |
| username, password); |
| |
| |
| setDetails(request, authRequest); |
| |
| return this.getAuthenticationManager().authenticate(authRequest); |
| } |
| } |
| |
PasswordEncoder
接口用于数据加密,即密码加密
点击查看源码
| package org.springframework.security.crypto.password; |
| |
| public interface PasswordEncoder { |
| String encode(CharSequence var1); |
| |
| boolean matches(CharSequence var1, String var2); |
| |
| default boolean upgradeEncoding(String encodedPassword) { |
| return false; |
| } |
| } |
| |
认证方式
- 新建一个springboot项目,导入security依赖,任意访问一个控制器中的方法都需要认证,用户名为user,密码在控制台
- 方式一:在配置文件yml中设置密码
- 方式二:在配置类中设置(继承WebSecurityConfigurerAdapter,重写configure和password方法)
- 方式三:在UserDetailsService实现类中查询数据库中的用户名和密码,将实现类注入配置类
点击查看实现类
| @Service("userDetailsService") |
| public class MyUserDetailsService implements UserDetailsService { |
| |
| @Override |
| public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { |
| |
| List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale"); |
| |
| return new User("admin", new BCryptPasswordEncoder().encode("123456"),auths); |
| } |
| |
| } |
| |
点击查看配置类
| @Configuration |
| public class SecurityConfig extends WebSecurityConfigurerAdapter { |
| |
| @Autowired |
| private UserDetailsService userDetailsService; |
| |
| @Override |
| protected void configure(AuthenticationManagerBuilder auth) throws Exception { |
| auth.userDetailsService(userDetailsService).passwordEncoder(password()); |
| } |
| |
| @Bean |
| PasswordEncoder password() { |
| return new BCryptPasswordEncoder(); |
| } |
| |
| } |
| |
- 在配置类中指定自定义的登录页面,不需要认证就能访问的url
点击查看配置类
| @Configuration |
| public class SecurityConfig extends WebSecurityConfigurerAdapter { |
| |
| @Autowired |
| private UserDetailsService userDetailsService; |
| |
| |
| @Autowired |
| private DataSource dataSource; |
| |
| |
| @Bean |
| public PersistentTokenRepository persistentTokenRepository() { |
| JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); |
| jdbcTokenRepository.setDataSource(dataSource); |
| return jdbcTokenRepository; |
| } |
| |
| @Bean |
| PasswordEncoder password() { |
| return new BCryptPasswordEncoder(); |
| } |
| |
| @Override |
| protected void configure(HttpSecurity http) throws Exception { |
| |
| |
| http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll(); |
| |
| |
| http.exceptionHandling().accessDeniedPage("/unauth.html"); |
| |
| |
| http.formLogin().loginPage("/login.html") |
| |
| .loginProcessingUrl("/login") |
| |
| .defaultSuccessUrl("/success.html") |
| |
| .failureUrl("/fail.html"); |
| |
| http.authorizeRequests().antMatchers("/login.html").permitAll() |
| |
| .anyRequest().authenticated() |
| .and().rememberMe().tokenRepository(persistentTokenRepository()) |
| |
| .tokenValiditySeconds(60) |
| .userDetailsService(userDetailsService); |
| |
| |
| } |
| |
| } |
| |
- 认证业务逻辑:访问控制器方法 -> 实现类中查询 -> 认证通过 -> 执行控制器方法
项目案例
下载地址
授权
- 在配置类中通过hasAuthority方法给指定url设置权限,在业务层实现类中给登录主体赋予权限
| .antMatchers("/test/index").hasAuthority("admins") |
| .antMatchers("/test/main").hasAuthority("user") |
| |
- 在配置类通过hasAnyAuthority方法给指定url设置多个权限,访问时,只要具有其一权限即可访问
| .antMatchers("/test/index").hasAnyAuthority("admin,manager") |
| |
| |
| AuthorityUtils.commaSeparatedStringToAuthorityList("admin,sale"); |
| |
- 在配置类中通过hasRole方法给指定url设置权限
| .antMatchers("/test/index").hasRole("sale") |
| |
- 在配置类中通过hasAnyRole方法给指定url设置多个权限,访问时,只需具有其中一个权限即可访问
| .antMatchers("/test/index").hasAnyRole("sale,admin") |
| |
| |
| AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin,ROLE_sale"); |
| |
注解方式授权
| |
| @EnableGlobalMethodSecurity(securedEnabled=true) |
| |
| @Secured({"ROLE_normal","ROLE_admin"}) |
| |
| .antMatchers("/test/index").hasAnyRole("sale,admin") |
| |
| |
| @PreAuthorize("hasAnyAuthority('admins,manager')") |
| |
| |
| |
| @PostAuthorize("hasAnyAuthority('admins,manager')") |
| |
| |
认证业务逻辑
- 自定义一个认证过滤器
TokenLoginFilter
继承UsernamePasswordAuthenticationFilter
,登录时输入用户名和密码,进入认证过滤器TokenLoginFilter
,获取登陆时的用户名和密码,进入UserDetailsServiceImpl
实现类,该实现类实现了UserDetailsService
接口,在UserDetailsServiceImpl
中根据用户名去数据库查询用户信息,返回securityUser对象,认证成功后进入认证过滤器中的successfulAuthentication方法
源码分析
- 自定义的认证过滤器
TokenLoginFilter
继承UsernamePasswordAuthenticationFilter
,UsernamePasswordAuthenticationFilter
继承了AbstractAuthenticationProcessingFilter
,在该类中的doFilter方法会判断该请求是否是post请求,不是则放行,是post则拦截认证
- 之后在
UsernamePasswordAuthenticationFilter
类中的attemptAuthentication方法会获取表单提交的数据,然后进行认证(查数据库,比较),认证通过后,将用户数据封装到Authentication
点击查看源码
| 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); |
| } |
| } |
| |
- 认证成功后在
AbstractAuthenticationProcessingFilter
类中会将数据存入session
- 认证失败时异常过滤器
ExceptionTranslationFilter
会抛出异常,执行unsuccessfulAuthentication方法;认证成功时则执行successfulAuthentication方法
attemptAuthentication方法源码分析
- 判断是否是post提交,不是则抛出异常,是post则继续执行
- 获取表单中提交的数据
- new一个UsernamePasswordAuthenticationToken对象,将表单提交的数据构建进该对象,并标记为未认证状态
| UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); |
| |
- 调用authenticate方法进行认证,该方法会调用
UserDetailsService
的实现类查数据库进行认证
UsernamePasswordAuthenticationToken对象构建源码分析
UsernamePasswordAuthenticationToken
继承了一个抽象类AbstractAuthenticationToken
- 在
UsernamePasswordAuthenticationToken
类中有两个方法,根据传入的参数调用指定的方法,调用UsernamePasswordAuthenticationToken
方法表示标记为未认证状态,调用UsernamePasswordAuthenticationToken
方法表示将对象标记未已认证状态
点击查看源码
| public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { |
| private static final long serialVersionUID = 540L; |
| private final Object principal; |
| private Object credentials; |
| |
| public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { |
| super((Collection)null); |
| this.principal = principal; |
| this.credentials = credentials; |
| this.setAuthenticated(false); |
| } |
| |
| public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { |
| super(authorities); |
| this.principal = principal; |
| this.credentials = credentials; |
| super.setAuthenticated(true); |
| } |
| |
| } |
| |
- 抽象类
AbstractAuthenticationToken
则是实现了Authentication
接口,该接口中包含一些用户信息
点击查看源码
| public interface Authentication extends Principal, Serializable { |
| Collection<? extends GrantedAuthority> getAuthorities(); |
| |
| Object getCredentials(); |
| |
| Object getDetails(); |
| |
| Object getPrincipal(); |
| |
| boolean isAuthenticated(); |
| |
| void setAuthenticated(boolean var1) throws IllegalArgumentException; |
| } |
| |
authenticate方法源码分析
- 调用authenticate方法来进行认证,该方法是
AuthenticationManager
接口中的方法,该接口的实现类ProviderManager
- 登录时表单提交的数据被封装进UsernamePasswordAuthenticationToken对象,然后传入authenticate方法;在
ProviderManager
实现类中authenticate方法会将UsernamePasswordAuthenticationToken对象中的信息迭代,之后判断该对象是否是UsernamePasswordAuthenticationToken类型,之后会调用authenticate方法将对象信息传入进行认证
| result = provider.authenticate(authentication) |
| |
- 认证失败抛出异常,认证成功则将查询到的details复制到authentication对象中
| 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; |
| } |
| |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 字符编码:从基础到乱码解决
· 提示词工程——AI应用必不可少的技术