Spring|Spring Security使用单点登录的权限功能
目录
- 背景
- Spring Security
- 实现
- 已经有了单点登录页面,Spring Security怎么登录,不登录可以拿到权限吗
- Authentication
- 继续分析
- 结论
- 如何手动登录Spring Security
- 结论
- 总结
- 附
- 参考
背景 在配置中心增加权限功能
- 目前配置中心已经包含了单点登录功能,可以通过统一页面进行登录,登录完会将用户写入用户表
- RBAC的用户、角色、权限表CRUD、授权等都已经完成
- 希望不用用户再次登录,就可以使用SpringSecurity的权限控制
Spring Security Spring Security最主要的两个功能:认证和授权
功能 | 解决的问题 | Spring Security中主要类 |
---|---|---|
认证(Authentication) | 你是谁 | AuthenticationManager |
授权(Authorization) | 你可以做什么 | AuthorizationManager |
实现 在这先简单了解一下Spring Security的架构是怎样的,如何可以认证和授权的
过滤器大家应该都了解,这属于Servlet的范畴,Servlet 过滤器可以动态地拦截请求和响应,以变换或使用包含在请求或响应中的信息
文章图片
DelegatingFilterProxy是一个属于Spring Security的过滤器
通过这个过滤器,Spring Security就可以从Request中获取URL来判断是不是需要认证才能访问,是不是得拥有特定的权限才能访问。
已经有了单点登录页面,Spring Security怎么登录,不登录可以拿到权限吗 Spring Security官方文档-授权架构中这样说,GrantedAuthority(也就是拥有的权限)被AuthenticationManager写入Authentication对象,后而被AuthorizationManager用来做权限认证
The GrantedAuthority objects are inserted into the Authentication object by the AuthenticationManager and are later read by either the AuthorizationManager when making authorization decisions.为了解决我们的问题,即使我只想用权限认证功能,也得造出一个Authentication,先看下这个对象:
Authentication Authentication包含三个字段:
- principal,代表用户
- credentials,用户密码
- authorities,拥有的权限
- AuthenticationManager的入参,仅仅是用来存用户的信息,准备去认证
- AuthenticationManager的出参,已经认证的用户信息,可以从SecurityContext获取
继续分析 看到这可以得到,要实现不登录的权限认证,只需要手动造一个Authentication,然后放入SecurityContext就可以了,先尝试一下,大概流程是这样,在每个请求上
- 获取sso登录的用户
- 读取用户、角色、权限写入Authentication
- 将Authentication写入SecurityContext
- 请求完毕时将SecurityContext清空,因为是ThreadLocal的,不然可能会被别的用户用到
- 同时Spring Security的配置中是对所有的url都允许访问的
import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @WebFilter( urlPatterns = "/*", filterName = "reqResFilter" )public class ReqResFilter implements Filter{ @Autowired private SSOUtils ssoUtils; @Autowired private UserManager userManager; @Autowired private RoleManager roleManager; @Override public void init( FilterConfig filterConfig ) throws ServletException{ } @Override public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain )throws IOException, ServletException{setAuthentication(servletRequest); filterChain.doFilter( servletRequest, servletResponse ); clearAuthentication(); } @Override public void destroy(){ } private void setAuthentication( ServletRequest request ){Map data; try{data = https://www.it610.com/article/ssoUtils.getLoginData( ( HttpServletRequest )request ); }catch( Exception e ){data = new HashMap<>(); data.put( "name", "visitor" ); }String username = data.get( "name" ); if( username != null ){userManager.findAndInsert( username ); }ListuserRole = userManager.findUserRole( username ); List roleIds = userRole.stream().map( Role::getId ).collect( Collectors.toList() ); List rolePermission = roleManager.findRolePermission( roleIds ); List authorities = rolePermission.stream().map( one -> new SimpleGrantedAuthority( one.getName() ) ).collect(Collectors.toList() ); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( username, "", authorities ); SecurityContextHolder.getContext().setAuthentication( authenticationToken ); } private void clearAuthentication(){SecurityContextHolder.clearContext(); }}
从日志可以看出,Principal: visitor,当访问未授权的接口被拒绝了
16:04:07.429 [http-nio-8081-exec-9] DEBUG org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@cc4c6ea0: Principal: visitor; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: CHANGE_USER_ROLE, CHANGE_ROLE_PERMISSION, ROLE_ADD...org.springframework.security.access.AccessDeniedException: 不允许访问
结论 不登录是可以使用Spring Security的权限,从功能上是没有问题的,但存在一些别的问题
- 性能问题,每个请求都需要请求用户角色权限数据库,当然可以利用缓存优化
- 我们写的过滤器其实也是Spring Security做的事,除此之外,它做了更多的事,比如结合HttpSession, Remember me这些功能
如何手动登录Spring Security How to login user from java code in Spring Security? 从这篇文章从可以看到,只要通过以下代码即可
private void loginInSpringSecurity( String username, String password ){UsernamePasswordAuthenticationToken loginToken = new UsernamePasswordAuthenticationToken( username, password ); Authentication authenticatedUser = authenticationManager.authenticate( loginToken ); SecurityContextHolder.getContext().setAuthentication( authenticatedUser ); }
和上面我们直接拿已经认证过的用户对比,这段代码让Spring Security来执行认证步骤,不过需要配置额外的AuthenticationManager和UserDetailsServiceImpl,这两个配置只是AuthenticationManager的一种实现,和上面的流程区别不大,目的就是为了拿到用户的信息和权限进行认证
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; @Servicepublic class UserDetailsServiceImpl implements UserDetailsService{ private static final Logger logger = LoggerFactory.getLogger( UserDetailsServiceImpl.class ); @Autowired private UserManager userManager; @Autowired private RoleManager roleManager; @Override public UserDetails loadUserByUsername( String username ) throws UsernameNotFoundException{User user = userManager.findByName( username ); if( user == null ){logger.info( "登录用户[{}]没注册!", username ); throw new UsernameNotFoundException( "登录用户[" + username + "]没注册!" ); }return new org.springframework.security.core.userdetails.User( user.getUsername(), "", getAuthority( username ) ); } private List extends GrantedAuthority> getAuthority( String username ){ListuserRole = userManager.findUserRole( username ); List roleIds = userRole.stream().map( Role::getId ).collect( Collectors.toList() ); List rolePermission = roleManager.findRolePermission( roleIds ); return rolePermission.stream().map( one -> new SimpleGrantedAuthority( one.getName() ) ).collect( Collectors.toList() ); }}
@Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception{DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService( userDetailsService ); daoAuthenticationProvider.setPasswordEncoder( NoOpPasswordEncoder.getInstance() ); return new ProviderManager( daoAuthenticationProvider ); }
结论 通过这样的方式,同样实现了权限认证,同时Spring Security会将用户信息和权限缓存到了Session中,这样就不用每次去数据库获取
总结 可以通过两种方式来实现不登录使用SpringSecurity的权限功能
- 手动组装认证过的Authentication直接写到SecurityContext,需要我们自己使用过滤器控制写入和清除
- 手动组装未认证过的Authentication,并交给Spring Security认证,并写入SecurityContext
附 Spring Security是如何配置的,因为只使用权限功能,所有允许所有的路径访问(我们的单点登录会限制接口的访问)
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 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.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.Arrays; import java.util.Collections; @Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired private UserDetailsService userDetailsService; @Override protected void configure( HttpSecurity http ) throws Exception{http.cors().and().csrf().disable().sessionManagement().and().authorizeRequests().anyRequest().permitAll().and().exceptionHandling().accessDeniedHandler( new SimpleAccessDeniedHandler() ); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception{DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService( userDetailsService ); daoAuthenticationProvider.setPasswordEncoder( NoOpPasswordEncoder.getInstance() ); return new ProviderManager( daoAuthenticationProvider ); } @Bean public CorsConfigurationSource corsConfigurationSource(){CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins( Collections.singletonList( "*" ) ); configuration.setAllowedMethods( Arrays.asList( "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS" ) ); configuration.setAllowCredentials( true ); configuration.setAllowedHeaders( Collections.singletonList( "*" ) ); configuration.setMaxAge( 3600L ); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration( "/**", configuration ); return source; }}
参考
- Spring Security官方文档-授权架构
- How to login user from java code in Spring Security?
推荐阅读
- 教你使用Jenkins集成Harbor自动发布镜像
- 使用|使用 ABAP Open SQL 的 Select AS 别名,提高代码可读性
- 使用|使用 SAP ABAP 封装的 Office Integration class 访问本地 Excel 文件
- JoJoGAN|JoJoGAN One-Shot Face Stylization(使用 StyleGAN 创建 JoJo风格人脸头像)
- BootstrapBlazor实战|BootstrapBlazor实战 Chart 图表使用(1)
- 开发工具|小马带你认识前端开发神器WebStorm(WebStorm及Git的相关配置与使用)
- 使用透明度实现Mask遮罩的Unity Shader
- Spring4Shell的漏洞原理分析
- Android 协程使用指南
- JavaScript中Infinity(无穷数)的使用和注意事项