Spring-Security权限框架
source link: https://maizitoday.github.io/post/spring-security权限框架/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
[TOC]
转载地址: http://www.spring4all.com/article/443
认证流程
授权流程
流程图
核心组件
SecurityContextHolder
SecurityContextHolder
用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保存在SecurityContextHolder中。 SecurityContextHolder
默认使用 ThreadLocal
策略来存储认证信息。看到 ThreadLocal
也就意味着,这是一种与线程绑定的策略。Spring Security在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。但这一切的前提,是你在web场景下使用Spring Security,而如果是Swing界面,Spring也提供了支持, SecurityContextHolder
的策略则需要被替换,鉴于我的初衷是基于web来介绍Spring Security,所以这里以及后续,非web的相关的内容都一笔带过。
获取当前用户的信息
因为身份信息是与线程绑定的,所以可以在程序的任何地方使用静态方法获取用户信息。一个典型的获取当前登录用户的姓名的例子如下所示:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { String username = ((UserDetails)principal).getUsername(); } else { String username = principal.toString(); }
getAuthentication()返回了认证信息,再次getPrincipal()返回了身份信息,UserDetails便是Spring对身份信息封装的一个接口。
Authentication
先看看这个接口的源码长什么样:
package org.springframework.security.core;// <1> public interface Authentication extends Principal, Serializable { // <1> Collection<? extends GrantedAuthority> getAuthorities(); // <2> Object getCredentials();// <2> Object getDetails();// <2> Object getPrincipal();// <2> boolean isAuthenticated();// <2> void setAuthenticated(boolean var1) throws IllegalArgumentException; }
java.security
提供基础认证数据接口
Spring Security是如何完成身份认证的
- 用户名和密码被过滤器获取到,封装成
Authentication
,通常情况下是UsernamePasswordAuthenticationToken
这个实现类 - AuthenticationManager
身份管理器负责验证这个
Authentication - 认证成功后,
AuthenticationManager
身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication
实例。 -
SecurityContextHolder
安全上下文容器将第3步填充了信息的Authentication
,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
AuthenticationManager
初次接触Spring Security的朋友相信会被 AuthenticationManager
, ProviderManager
, AuthenticationProvider
…这么多相似的Spring认证类搞得晕头转向,但只要稍微梳理一下就可以理解清楚它们的联系和设计者的用意。AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录(还有这样的操作?没想到吧),所以说AuthenticationManager一般不直接认证,AuthenticationManager接口的常用实现类 ProviderManager
内部会维护一个 List<AuthenticationProvider>
列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。也就是说,核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式:用户名+密码(UsernamePasswordAuthenticationToken),邮箱+密码,手机号码+密码登录则对应了三个AuthenticationProvider。这样一来四不四就好理解多了?熟悉shiro的朋友可以把AuthenticationProvider理解成Realm。在默认策略下,只需要通过一个AuthenticationProvider的认证,即可被认为是登录成功。
ProviderManager 中的List<AuthenticationProvider>,会依照次序去认证,认证成功则立即返回,若认证失败则返回null,下一个AuthenticationProvider会继续尝试认证,如果所有认证器都无法认证成功,则
ProviderManager` 会抛出一个ProviderNotFoundException异常。
进行数据认证接口
到这里,如果不纠结于AuthenticationProvider的实现细节以及安全相关的过滤器,认证相关的核心类其实都已经介绍完毕了:身份信息的存放容器SecurityContextHolder,身份信息的抽象Authentication,身份认证器AuthenticationManager及其认证流程。姑且在这里做一个分隔线。下面来介绍下AuthenticationProvider接口的具体实现。
DaoAuthenticationProvider
AuthenticationProvider最最最常用的一个实现便是DaoAuthenticationProvider。顾名思义,Dao正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。
按照我们最直观的思路,怎么去认证一个用户呢?用户前台提交了用户名和密码,而数据库中保存了用户名和密码,认证便是负责比对同一个用户名,提交的密码和保存的密码是否相同便是了。在Spring Security中。提交的用户名和密码,被封装成了UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了UserDetailsService,在DaoAuthenticationProvider中,对应的方法便是retrieveUser,虽然有两个参数,但是retrieveUser只有第一个参数起主要作用,返回一个UserDetails。还需要完成UsernamePasswordAuthenticationToken和UserDetails密码的比对,这便是交给additionalAuthenticationChecks方法完成的,如果这个void方法没有抛异常,则认为比对成功。比对密码的过程,用到了PasswordEncoder和SaltSource,密码加密和盐的概念相信不用我赘述了,它们为保障安全而设计,都是比较基础的概念。
如果你已经被这些概念搞得晕头转向了,不妨这么理解DaoAuthenticationProvider: 它获取用户提交的用户名和密码,比对其正确性,如果正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)。
UserDetails与UserDetailsService
UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已,记住这一点,可以避免走很多弯路。UserDetailsService常见的实现类有JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,也可以自己实现UserDetailsService,通常这更加灵活。我们一般都是
public class JwtUserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 查询数据库,填充数据,主要是填充 username 和 password,他主要是通过这两个值判断。 // 同时你也可以添加扩展属性,保存值,成功后,用户数据都在这里面 return null; } }
WebSecurityConfiguration
WebSecurityConfiguration中完成了声明springSecurityFilterChain的作用,并且最终交给DelegatingFilterProxy这个代理类,负责拦截请求(注意DelegatingFilterProxy这个类不是spring security包中的,而是存在于web包中,spring使用了代理模式来实现安全过滤的解耦)。
WebSecurityConfigurerAdapter
适配器模式在spring中被广泛的使用,在配置中使用Adapter的好处便是,我们可以选择性的配置想要修改的那一部分配置,而不用覆盖其他不相关的配置。WebSecurityConfigurerAdapter中我们可以选择自己想要修改的内容,来进行重写,而其提供了三个configure重载方法,是我们主要关心的
分别是对AuthenticationManagerBuilder,WebSecurity,HttpSecurity进行个性化的配置。
HttpSecurity
转载地址: https://www.jianshu.com/p/6f1b129442a1
-
HttpSecurity
最终可以得到一个DefaultSecurityFilterChain
通过的是build()
方法 -
HttpSecurity
维护了一个过滤器的列表,这个过滤器的列表最终放入了DefaultSecurityFilterChain
这个过滤器链中 -
HttpSecurity
最终提供了很多的配置,然而所有的配置也都是为了处理维护我们的过滤器列表
允许基于选择匹配在资源级配置基于网络的安全性
http.authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll().anyRequest() .authenticated();
WebSecurity
转载地址: https://blog.csdn.net/andy_zhang2007/article/details/82111647
WebSecurity
是 Spring Security
的一个 SecurityBuilder
。它的任务是基于一组 WebSecurityConfigurer
构建出一个 Servlet Filter
,具体来讲就是构建一个 Spring Security
的 FilterChainProxy
实例。这个 FilterChainProxy
实现了 Filter
接口,也是通常我们所说的 Spring Security Filter Chain
,所构建的 FilterChainProxy
实例缺省会使用名称 springSecurityFilterChain
作为 bean
注册到容器,运行时处理 web
请求过程中会使用该 bean
进行安全控制。
每个 FilterChainProxy
包装了一个 HttpFirewall
和若干个 SecurityFilterChain
, 这里每个 SecurityFilterChain
要么对应于一个要忽略安全控制的 URL
通配符( RequestMatcher
);要么对应于一个要进行安全控制的 URL
通配符( HttpSecurity
)。
助记公式 : 1 FilterChainProxy = 1 HttpFirewall + n SecurityFilterChain
用于影响全局安全性(配置资源,设置调试模式,通过实现自定义防火墙定义拒绝请求)的配置设置。一般用于配置全局的某些通用事物,例如静态资源等
@Override public void configure(WebSecurity web) throws Exception { web.httpFirewall(httpFirewall); web.ignoring().antMatchers(SerurityConstant.IGNORING_URL); }
HttpFirewall
转载地址: https://blog.csdn.net/andy_zhang2007/article/details/90511688
StrictHttpFirewall
是 Spring Security Web
提供的一个 HTTP
防火墙(对应概念模型接口 HttpFirewall
)实现,该实现采用了严格模式,遇到任何可疑的请求,会通过抛出异常 RequestRejectedException
拒绝该请求。 StrictHttpFirewall
也是 Spring Security Web
在安全过滤器代理 FilterChainProxy
内置缺省使用的 HTTP
防火墙机制。
@Override public void configure(WebSecurity web) throws Exception { web.httpFirewall(httpFirewall); web.ignoring().antMatchers(SerurityConstant.IGNORING_URL); } /** * @Description: 自定义防火墙 * @Date: 2020-11-19 10:45:02 * @param {*} * @return {*} * @LastEditors: Do not edit */ @Bean public HttpFirewall httpFirewall() { StrictHttpFirewall firewall = new StrictHttpFirewall(); firewall.setAllowSemicolon(true); firewall.setAllowUrlEncodedDoubleSlash(true); firewall.setAllowUrlEncodedPercent(true); firewall.setAllowBackSlash(true); firewall.setAllowUrlEncodedSlash(true); firewall.setAllowUrlEncodedPeriod(true); return firewall; }
AuthenticationManagerBuilder
用于通过允许AuthenticationProvider容易地添加来建立认证机制。也就是说用来记录账号,密码,角色信息。
@Autowired public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); }
PasswordEncoder
密码进行加盐处理,我们也可以在注册用户的时候,对MD5的密码进行加盐处理,提高密码。
/** * 加密密码 */ private void encryptPassword(UserEntity userEntity){ String password = userEntity.getPassword(); password = new BCryptPasswordEncoder().encode(password); userEntity.setPassword(password); }
授权注解
//开启全局方法调用权限验证 // 当@EnableGlobalMethodSecurity(prePostEnabled=true)的时候,@PreAuthorize @PostAuthorize才可以使用 @EnableGlobalMethodSecurity(prePostEnabled = true)
@PreAuthorize方法之前调用, 我们一般都是用这个。
@Service public class UserServiceImpl implements UserService { @PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasAnyRole('normal','admin')") public void addUser(User user) { System.out.println("addUser................" + user); } @PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('normal') AND hasRole('admin')") public User find(int id) { System.out.println("find user by id............." + id); return null; } }
public class UserServiceImpl implements UserService { /** * 限制只能查询Id小于10的用户 */ @PreAuthorize("#id<10") public User find(int id) { System.out.println("find user by id........." + id); return null; } /** * 限制只能查询自己的信息 */ @PreAuthorize("principal.username.equals(#username)") public User find(String username) { System.out.println("find user by username......" + username); return null; } /** * 限制只能新增用户名称为abc的用户 */ @PreAuthorize("#user.name.equals('abc')") public void add(User user) { System.out.println("addUser............" + user); } }
一般我们都是基于权限进行授权, 这样更加的灵活。
@PreAuthorize("hasAuthority('user:add')") @PreAuthorize("hasAnyAuthority('user:add','user:del')") public void addUser(User user) { System.out.println("addUser................" + user); }
总结:通过扫描 preAuthorize 注解, 查看我们封装的
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { QueryWrapper<UserEntity> queryWrapper = new QueryWrapper<UserEntity>(); queryWrapper.eq("user_name", username); UserEntity userEntity = userServiceImpl.getOne(queryWrapper); JwtUser jwtUser = null; if (userEntity != null) { String password = passwordEncoder.encode(userEntity.getPassWord()); // 在这个方法里面,进行对 GrantedAuthority 这个对象的填充,里面就是user:add等值 List<GrantedAuthority> authorityList = getAuthorityList(userEntity); QueryWrapper<UserRoleEntity> roleQw = new QueryWrapper<UserRoleEntity>(); roleQw.eq("fk_user_id", userEntity.getId()).select("fk_role_id", "fk_user_id"); List<UserRoleEntity> userRoleList = userRoleServiceImpl.list(roleQw); jwtUser = new JwtUser(username, password, 0, authorityList, userEntity.getId(), userRoleList); } else { log.debug("账号不存在"); handlerExceptionResolver.resolveException(request, response, null, new MyException(ResultCodeEnmu.NO_USER.description, ResultCodeEnmu.NO_USER.code)); } return jwtUser; }
是否有这个字符串,如果有的话就是权限ok。
自定义异常抓取
如何抛出自定义异常
import org.springframework.web.servlet.HandlerExceptionResolver; @Autowired private HandlerExceptionResolver handlerExceptionResolver; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { handlerExceptionResolver.resolveException(request, response, null, new MyException(ResultCodeEnmu.NO_USER.description, ResultCodeEnmu.NO_USER.code)); } return jwtUser; }
然后在全局异常就可以抓取这个异常
@ExceptionHandler(value = MyException.class) public Map<String, Object> myExceptionHandler(HttpServletRequest req, MyException e) { ResultDataUtil.ResponseDataFormatEnmu exceptionFormat = ResultDataUtil.ResponseDataFormatEnmu.RESPONSE_FORMAT_CUSTOM_EXCEPTION; exceptionFormat.setMsg(getTrace(e)); exceptionFormat.setRequestUrl(req.getRequestURL().toString()); exceptionFormat.setBusinessMsg(e.getExceptionMsg()); exceptionFormat.setParamsObj(req.getParameterMap()); HashMap<String, Object> resopnseData = exceptionFormat.getResultData(); log.error("系统业务日志", getTrace(e)); // e.printStackTrace(); return resopnseData; }
@PreAuthorize 注解的异常
@PreAuthorize 注解的异常,抛出AccessDeniedException异常,不会被accessDeniedHandler捕获,而是会被全局异常捕获。
动态权限修改示例
说明:列子来源网上,忘记地址了。
@GetMapping("/vip/test") @Secured("ROLE_VIP") // 需要ROLE_VIP权限可访问 public String vipPath() { return "仅 ROLE_VIP 可看"; }
@GetMapping("/vip") public boolean updateToVIP() { // 得到当前的认证信息 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); // 生成当前的所有授权 List<GrantedAuthority> updatedAuthorities = new ArrayList<>(auth.getAuthorities()); // 添加 ROLE_VIP 授权 updatedAuthorities.add(new SimpleGrantedAuthority("ROLE_VIP")); // 生成新的认证信息 Authentication newAuth = new UsernamePasswordAuthenticationToken(auth.getPrincipal(), auth.getCredentials(), updatedAuthorities); // 重置认证信息 SecurityContextHolder.getContext().setAuthentication(newAuth); return true; }
假设当前你的权限只有 ROLE_USER。那么按照上面的代码:
1、直接访问 /vip/test 路径将会得到403的Response;
2、访问 /vip 获取 ROLE_VIP 授权,再访问 /vip/test 即可得到正确的Response。
自定义 Filter
说明:列子来源网上,忘记地址了。
自定义的 Filter 建议继承 GenericFilterBean,本文示例:
public class BeforeLoginFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("This is a filter before UsernamePasswordAuthenticationFilter."); // 继续调用 Filter 链 filterChain.doFilter(servletRequest, servletResponse); } }
配置自定义 Filter 在 Spring Security 过滤器链中的位置
配置很简单,本文示例:
protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/").permitAll() .antMatchers("/user/**").hasRole("USER") .and() .formLogin().loginPage("/login").defaultSuccessUrl("/user") .and() .logout().logoutUrl("/logout").logoutSuccessUrl("/login"); // 在 UsernamePasswordAuthenticationFilter 前添加 BeforeLoginFilter http.addFilterBefore(new BeforeLoginFilter(),UsernamePasswordAuthenticationFilter.class); // 在 CsrfFilter 后添加 AfterCsrfFilter http.addFilterAfter(new AfterCsrfFilter(), CsrfFilter.class); }
说明: HttpSecurity 有三个常用方法来配置:
- addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter) 在 beforeFilter 之前添加 filter
- addFilterAfter(Filter filter, Class<? extends Filter> afterFilter) 在 afterFilter 之后添加 filter
- addFilterAt(Filter filter, Class<? extends Filter> atFilter) 在 atFilter 相同位置添加 filter, 此 filter 不覆盖 filter
通过在不同 Filter 的 doFilter() 方法中加断点调试,可以判断哪个 filter 先执行,从而判断 filter 的执行顺序 。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK