单点登录系统使用Spring Security的权限功能

背景

在配置中心增加权限功能

  • 目前配置中心已经包含了单点登录功能,可以通过统一页面进行登录,登录完会将用户写入用户表
  • RBAC的用户、角色、权限表CRUD、授权等都已经完成
  • 希望不用用户再次登录,就可以使用SpringSecurity的权限控制

Spring Security

Spring Security最主要的两个功能: 认证和授权

功能 解决的问题 Spring Security中主要类 认证(Authentication) 你是谁 AuthenticationManager 授权(Authorization) 你可以做什么 AuthorizationManager

实现

在这先简单了解一下Spring Security的架构是怎样的,如何可以认证和授权的

过滤器大家应该都了解,这属于Servlet的范畴,Servlet 过滤器可以动态地拦截请求和响应,以变换或使用包含在请求或响应中的信息

单点登录系统使用Spring Security的权限功能

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获取

SecurityContext和SecurityContextHolder用来存储Authentication, 通常是用了线程全局变量ThreadLocal, 也就是认证完成把Authentication放入SecurityContext,后续在整个同线程流程中都可以获取认证信息,也方便了认证

继续分析

看到这可以得到,要实现不登录的权限认证,只需要手动造一个Authentication,然后放入SecurityContext就可以了,先尝试一下,大概流程是这样,在每个请求上

  1. 获取sso登录的用户
  2. 读取用户、角色、权限写入Authentication
  3. 将Authentication写入SecurityContext
  4. 请求完毕时将SecurityContext清空,因为是ThreadLocal的,不然可能会被别的用户用到
  5. 同时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 = 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 );
        }
        List userRole = 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: &#x4E0D;&#x5141;&#x8BB8;&#x8BBF;&#x95EE;

结论

不登录是可以使用Spring Security的权限,从功能上是没有问题的,但存在一些别的问题

  • 性能问题,每个请求都需要请求用户角色权限数据库,当然可以利用缓存优化
  • 我们写的过滤器其实也是Spring Security做的事,除此之外,它做了更多的事,比如结合HttpSession, Remember me这些功能

我们可以采取另外一种做法,对用户来说只登录一次就行,我们仍然是可以手动用代码再去登录一次Spring Security的

如何手动登录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;

@Service
public 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 getAuthority( String username ){

        List userRole = 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的权限功能

  1. 手动组装认证过的Authentication直接写到SecurityContext,需要我们自己使用过滤器控制写入和清除
  2. 手动组装未认证过的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;
    }

}

参考

Original: https://www.cnblogs.com/songjiyang/p/16093636.html
Author: songtianer
Title: 单点登录系统使用Spring Security的权限功能

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/571684/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 设计模式之建造者模式

    在有些情况下,一个对象会有一些重要的性质,在他们没有被赋值之前,对象不能作为一个完整的产品使用。比如,一个电子邮件有发件人地址、收件人地址、主题、内容、附件等,最起码在收件人地址没…

    Java 2023年6月5日
    0122
  • 写了个简洁的Typora+Markdown简历模板

    项目地址:https://github.com/CodingDocs/typora-markdown-resume (欢迎小伙伴们使用!个人能力有限,也欢迎小伙伴们一起完善这个简历…

    Java 2023年6月9日
    087
  • Spring与Web环境集成

    Spring与Web环境集成 1. ApplicationContext应用上下文获取方式 应用上下文对象是通过 new ClassPathXmlApplicationContex…

    Java 2023年6月5日
    0115
  • Oracle备份与还原(实用版)

    Oracle备份与还原 EXP&#x548C;IMP&#x662F;&#x5BA2;&#x6237;&#x7AEF;&#x5DE5;…

    Java 2023年6月8日
    095
  • 《回炉重造》——集合(容器)

    整体框架 绿色代表接口/抽象类;蓝色代表类。 主要由两大接口组成,一个是「Collection」接口,另一个是「Map」接口。 前言 以前刚开始学习「集合」的时候,由于没有好好预习…

    Java 2023年6月10日
    083
  • Java之JavaWeb项目开发开始准备

    操作系统:Mac OS 10.11.6 Tomcat版本:9.0.0.M17 前言:部署Tomcat可以参考我一年前做的笔记:《在MAC下搭建JSP开发环境》,也可以参考大神写的挺…

    Java 2023年5月29日
    089
  • java学习之反射机制

    0x00前言和思维导图 1.反射机制定义:java反射机制是指在java代码执行过程中,对于任意一个类,可以获取这个类的属性与方法;对于任意一个对象,可以获取、修改这个对象的属性值…

    Java 2023年6月13日
    084
  • springboot自动配置原理

    从main函数说起 一切的开始要从SpringbootApplication注解说起。 @SpringBootApplication public class MyBootAppl…

    Java 2023年5月30日
    088
  • ftp多文件压缩下载

    @GetMapping(value = "/find") public String findfile(String filePath, String file…

    Java 2023年6月9日
    078
  • 正则表达式的点星匹配

    1 # -*- coding: utf-8 -*- 2 """ 3 Created on Mon Apr 20 22:51:44 2020 4 5 @…

    Java 2023年6月6日
    0118
  • Linux常用指令

    Linux常用指令 文件目录类 linux系统文件目录结构 当前工作目录的绝对路径(pwd 指令) 基本语法 pwd 功能描述:显示当前工作目录的绝对路径 显示文件或目录(ls 指…

    Java 2023年6月5日
    074
  • javax.net.ssl.SSLException: Certificate doesn’t match any of the subject alternative names

    问题:在使用 org.apache.http.*下的 CloseableHttpClient 发送https请求时报了以上错误 解决方案一:使用java.net.HttpURLCo…

    Java 2023年5月29日
    0133
  • xhydra的基础使用

    console–xhydra 在Target-Single Target输入目标IP(单个) 而Target List是批量爆破 Port:对应端口 Protocol:…

    Java 2023年6月7日
    071
  • 如何在电脑上配置Vue开发环境

    一,开发环境 : Node JS(npm) Visual Studio Code(前端IDE) 安装NodeJS 下载地址: nodejs中文网 Visual Studio Cod…

    Java 2023年6月15日
    090
  • 油猴插件安装以及好用的脚本推荐

    现在浏览器不搞几个插件和IE浏览器有啥区别,因此今天推荐一下及其强力的油猴(Tampermonkey)插件。 一、Tampermonkey插件安装 想使用插件首先要安装插件,我这里…

    Java 2023年6月13日
    093
  • 纯注解开发模式

    定义bean: 纯注解开发模式: 用SpringConfig类来代替applicationContext.xml配置文件,利用注解@configuration代表了xml里的基本配…

    Java 2023年6月16日
    096
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球