Spring Security 新理解

简介

本章节为SpringSecurity的二次深入了解

 

认证

认证是指用户登陆系统的登陆校验过程。

Spring Security 实际上是由多个拦截器(或叫过滤器)对用户访问的请求层层拦截和过滤,如下图,我们可以看到

说一下它们之间的关系:

  • UsernamePasswordAuthenticationFilter 是其中一个拦截器,它是负责检测用户访问请求时有没有已登陆,它会在内部使用 AuthenticationManager.authenticate( AuthenticationManager 实现类对象 ) 方法进行认证操作。
  • AuthenticationManager 是一个认证管理器,它是一个用于控制 Authentication 对象认证的工具类
  • Authentication 是一个接口标准,定义了一套登陆系统的接口,而其中的 UsernamePasswordAuthenticationToken 则是这个接口的其中一个实现类,采用用户密码登陆模式登陆系统,如果想使用其它方式登陆系统(如二维码登陆),则需要使用其它它的实现类。详情可以看它下属的实现类。
  • AuthenticationManager.authenticate() 会拿着实现类的认证信息进行认证,则最终会调用 loadUserByUsername 方法。
  • loadUserByUsername 方法可以重写,属于我们自己的登陆逻辑,比如改为Mysql查询用户等
  • loadUserByUsername 会返回由Security提供的接口 UserDetail 接口对象,UserDetail 是一个接口,所以我们需要创建一个实现类来定义 UserDetail 的类,UserDetail 这个接口实际上就是由Security规定的登陆成功后的登陆用户数据的标准POJO实体类,只有标准化这个类,Security才能正确的外理登陆用户的各种权限操作。
  • AuthenticationManager.authenticate() 返回的依然是一个 Authentication 对象,但这个对象和上面 AuthenticationManager 接收的 Authentication 对象不同的是,这个对象是 AuthenticationManager 接收的 Authentication 对象后,经过上图流程走完了,所携带重要信息的 Authentication 对象,如果上图的流程没走完,Authentication 对象将为null 
  • Authentication对象.getPrincipal() 实际上拿的就是 UserDetail 类对象
  • 注意:如果我们直接控制 AuthenticationManager 来实现自定义认证的话,需要把 AuthenticationManager 注入到IOC容器中。

 

自定义认证过滤器

通过上图我们知道,AuthenticationManager 是控制 Authentication 对象的,而 Authentication 就是一个认证的规范对象

那么Security中的过滤器们是如何感知到 Authentication 对象的状态是【已认证】还是【未认证】呢

其实是使用了一个全局类 SecurityContextHolder

过滤器们可以通过 SecurityContextHolder 获得 Authentication 对象,从而判断当前请求是否通过认证,从而做出相应操作。

那么我们就可以自定义一个过滤器,利用 Jwt 和 Redis 解密 token 得到 LoginDetail 对象,并包装成 Authentication 存入 SecurityContextHolder 中,并放行给后面的过滤器,这样不就能自定义控制请求是否认证的操作。

所以我们做一个过滤器:


/**
 * 创建一个自定义的过滤器
 * 用于增加在请求时接收token并使用JWT解密认证信息
 * 前置知识:
 * Authentication 对象作为整个Security的认证对象,所有与用户登陆认证的信息都以它为准
 * 其中包括【授权】、【这个对象是否认证通过】、【认证用户对象LoginDetail】等
 * 其中【这个对象是否认证通过】是其它过滤器作为判断用户成功登陆的依据
 * 它们会在 SecurityContextHolder 中获得 Authentication 对象进行判断
 * <p>
 * 所以本过滤器的作用就是抓取请求中的token并用Jwt解密出UserId,并通过在Redis中获取完整的用户信息LoginDetail对象
 * 并把对象封装为 Authentication 对象,后再存入 SecurityContextHolder 中
 * 方便使其它过滤器利用 SecurityContextHolder 获得 Authentication 对象进行判断该请求是否为已认证请求
 * <p>
 * 这个过滤器应当排在其它会抓取 SecurityContextHolder 对象的过滤器之前
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        String token = httpServletRequest.getHeader("token");
        if (!StringUtils.hasText(token)) {
            /**
             * 先判断 token 是否存在,如果不存在,则放行给后面的过滤器
             * 后面的过滤器会通过抓取 SecurityContextHolder 从而知道请求是否已认证
             * 我没有在这里放置 SecurityContextHolder 那么放行给后面的过滤器自然就失道是没认证的
             */
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }

        /**
         * 如果 token 是存在的,那么就要对token进行解密,这里还要考虑到一个问题是token是否合法的问题
         * 如果不合法(或已过期),那么我们可以选择抛出异常(会被异常过滤器所捕获),
         * 又或者直接放行,使得后面的过滤器知道该请求是没认证的也行
         *
         * 这里就直接使用抛出异常处理了
         */
        Long userId;
        try {
            userId = JWTUtil.getId(token);
        } catch (RuntimeException e) {
            e.printStackTrace();
            throw new RuntimeException("token不合法,有可能token已过期");
        }

        /**
         * 如果 Jwt 解密后的 UserId 能成功获取的话,说明 token 是有效的,这时我们
         * 1.通过 UserId 在 Redis 中获取 LoginDetail 数据
         * 2.把 LoginDetail 数据先包装成 Authentication 对象中,
         *   因为 SecurityContextHolder 需要的就是 Authentication 对象。
         * 3.把获取到的 LoginDetail 数据存入 SecurityContextHolder 中
         *
         * SecurityContextHolder 存在已认证的 Authentication 对象,在后续的过滤器中便知道
         * 这个请求是已认证的请求。
         */
        String LoginDetailJson = stringRedisTemplate.opsForValue().get(SystemConstant.getLoginUserIdKey(userId));
        LoginDetail loginDetail = JSON.parseObject(LoginDetailJson, LoginDetail.class);

        /**
         * 在把 LoginDetail 对象封装成 Authentication 对象是需要注意
         * Authentication 中有一个方法 isAuthenticated() 就是用于存储【这个对象是否认证通过】的
         * 所以我们在封装 Authentication 对象时,要注意必须使 isAuthenticated() 为 true
         * 否则后面的过滤器依然会认为我们的请求是未认证请求
         *
         * UsernamePasswordAuthenticationToken 是 Authentication 的实现类.
         *  - 当然我们也可以通过实现 Authentication 的方法自定义登陆规范
         * 构造方法有两个:
         * 一个是用于创建未认证的 Authentication 对象,isAuthenticated() 为 false
         * 一个是用于创建已认证的 Authentication 对象,isAuthenticated() 为 true
         * 注意我们要使用后者(包括以后使用其它认证方式的实现类也同样注意)
         *  @参数1: 用于存储认证的基础信息,如果是已认证的,那么就是用来存储已认证的数据,如果是未认证的,则用来存储待登陆用户名
         *  @参数2: 用于存储认证的敏感信息,如果是已认证的,那么可以不存东西,如果是未认证的,则它是用来存待登陆的用户密码
         *  @参数3: 用于存储已认证账户的授权信息,既然是已认证的,那么这个用户就有允许访问的授权范围,
         *          通常是一个数组,比如是否允许增删改查,则以数组成员来存储,当然这个是由开发者去规定授权名如何定义
         */

        UsernamePasswordAuthenticationToken user = new UsernamePasswordAuthenticationToken(loginDetail, null, loginDetail.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(user);
        filterChain.doFilter(httpServletRequest, httpServletResponse);

    }
}

注意:这个过滤器应当放置在会调用 SecurityContextHolder 类的过滤器之前。

 

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        // 关闭session,因为我们使用的是前后分离的
        httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        // 注册增加自定义过滤器
        httpSecurity.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        // 关闭 csrf 跨域限制
        httpSecurity.csrf().disable();
        return httpSecurity.build();
    }

 

退出登陆

因为我们把登陆的用户信息存在Redis中,所以如果我们遇到了退出登陆的情况(非登陆过期),我们应该在Redis中删除对应的UserId的记录,我们可以通过使用全局类 SecurityContextHolder 获取。

    @Override
    public ResultResponse<String> doLogout() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginDetail loginDetail = (LoginDetail) authentication.getPrincipal();
        stringRedisTemplate.delete(SystemConstant.getLoginUserIdKey(loginDetail.getUser().getId()));
        return ResultResponse.success("退出成功");
    }

 

 

授权

授权是指用户登陆进系统后对用户限制允许访问的围范

从上面节章我们知道,Security 设计了一个用于定义用户登陆的接口规范 Authentication,它可以让各种过滤器识别当前请求是否为已认证的请求,同时这个这个接口规范中也包含了系统权限验证的部分,我们可以通过定制Authentication中的成员变量来让其它过滤器识别出来当前的登陆用户是否满足访问当前请求的权限要求。

Authentication 中用于存储授权信息的,就是接口方法中的 getAuthorities() 方法。

  • 授权访问是一个相呼应的规则,即访问者那边拿着授权令牌去请求,请求这边也要同样拿着可授权名单,对访问者中的授权令牌进行匹配,只有成功匹配,我们才认为它有权限访问这个接口。
  • 所以即使我们的 Authentication 对象中包含了相应的授权,但同时我们也要对各个接口设置指定的可访问名单。

 

基于配置的定义授权

通常我们可以对一系列的请求进行批量设置访问名单,这种时候我们就可以在 HttpSecurity 中定义,这种方式的定义我们称为基于配置的授权定义。

举例说明:对于某种匹配的url地址的所有请求都只能允许带有“admin”权限的用户访问

httpSecurity.authorizeRequests().antMatchers("/admin/**").hasAuthority("admin");

在配置中,我们创建了一个地址匹配“/admin/**”,并让它授权 getAuthorities() 中带有“admin”权限的 Authentication 对象才允许访问。

相似的规则还有:

  • hasRole(String) : Security 自动在规则中加上“ROLE_”后的整体名称的角色名,少用,因为它加了“ROLE_”
  • hasAnyRole(String...) : hasRole(String) 的同时设置多个角色名版本
  • hasAnyAuthority(String...) : 可以同时设置多个权限的角色名,不会加上“ROLE_”的,和 hasAuthority() 一样的
  • hasIpAddress(ip) : 限制只有设置的IP才可以访问该接口

 

 

基于注解的定义授权

在 基于配置的定义授权 章节中我们可以通过代码配置的方式同时对多个url接口设置单个或多个可访问名单,但若我希望只在某单一url上配置特定的访问名单的时候,就需要使用注解方式定义授权了,可以在每一个url请求中使用注解来配置特定的访问权限要求。

步骤:

  • 我们需要开启注解方式授权 @EnableGlobalMethodSecurity(prePostEnabled = true)
  • @EnableGlobalMethodSecurity(prePostEnabled = true)
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter
  • 在每个方法上使用预访问前权限校验注解 @PreAuthorize 来定义当用户访问请求之前,对用户的权限做校验
  •     @GetMapping("/logout")
        @PreAuthorize("hasAuthority('admin')")
        public ResultResponse<String> logout(){
            return loginService.doLogout();
        }
  • @PreAuthorize 需要提供一种校验授权方式的模式,在参数中提供这种模式,和匹配的授权权限即可。
    • hasRole(String) : Security 自动在规则中加上“ROLE_”后的整体名称的角色名,少用,因为它加了“ROLE_”
    • hasAnyRole(String...) : hasRole(String) 的同时设置多个角色名版本
    • hasAnyAuthority(String...) : 可以同时设置多个权限的角色名,不会加上“ROLE_”的,和 hasAuthority() 一样
    • hasIpAddress(ip) : 限制只有设置的IP才可以访问该接口

 

配置 Authentication 对象中的拥有权限

Security 中提供的 Authentication 对象封装了用于获取授权信息的方法 getAuthorities() ,主要讲解如何把我们自己定义的授权封装成 Authentication 规范的授权。

Authentication 的 getAuthorities() 方法定义如下:

public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
}

它需要我们提供一个 GrantedAuthority 类型的集合,GrantedAuthority 是一个接口,用于规范各种不同类型的授权定义的标准,比如我们使用的最多的是用 字符串(“admin”)这样来定义该用户有 admin 权限,但不是所有系统都会使用字符串做存储授权数据,所以Security 设计了 GrantedAuthority 接口,以符合那些希望用个性化方式存储授数据的开发者。

但Security 提供了三个GrantedAuthority接口的实现类:

  • JaasGrantedAuthority : 使用 Principal 类作为授权存储介质的方式(即只允许带有这些Principal对象的(如把LoginDetail作为Principal))
  • SimpleGrantedAuthority : 使用 String 字符串作为授权存储介质的方式(即只允许带有这些字符匹配的)
  • SwitchUserGrantedAuthority : 使用 Authentication 对象作为授权存储介质的方式(即只允许这些Authentication对象)

其中 SimpleGrantedAuthority 正是我们想要的通过字符来匹配的方式,所以我们可以在LoginDetail中获得拥有的权限信息(这些可以在数据库中获取后包装到LoginDetail中)封装为GrantedAuthority集合中。

    private List<String> permissions;  // 在数据库中查出来的该用户的权限列(字符串形式)

    private Set<GrantedAuthority> authorities = new HashSet<>(); // 通过 permission 转修为 Security 识别的权限列(GrantedAuthority形式) 
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        
        // 如果 authorities 本身不为空,直接返回,就不用每次都转一次了
        if (!CollectionUtils.isEmpty(authorities))
        {
            return authorities;
        }
        
        for (String permission : permissions) {
            // 一个权限信息封装一个 GrantedAuthority 对象
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
            authorities.add(simpleGrantedAuthority);
        }
        return authorities;
    }

那么,这个 Authentication 就拥有相应的授权了,在下次访问url接口时,接口就会和 Authentication  匹配权限,只有匹配成功才给访问。

 

自定义权限校验算法

在一些特殊情况下,Security官方提供的权限校验方法有可能会不完全满足于我们的业务需求,那么我们就可以通过自定义一个返回值为 boolean 的方法,使用spEL语法在@PreAuthorize注解中使用我们自己定义的权限校验处理方式

创建一个方法,用于验证权限

@Component("cusExpressionRoot")
public class CusExpressionRoot {
    public boolean cusHasAuthority(String permission){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginDetail loginDetail = (LoginDetail) authentication.getPrincipal();
        List<String> permissions = loginDetail.getPermissions();
        return permissions.contains(permission);
    }
}

 

在控制器方法@PreAuthorize注解上,使用spEL语法访问IoC容器中的对象中的方法调用判断是否允许访问

    @GetMapping("/logout")
    @PreAuthorize("@cusExpressionRoot.cusHasAuthority('system:login')")
    public ResultResponse<String> logout(){
        return loginService.doLogout();
    }

 

 

自定义异常处理

Security 中提供了异常处理的过滤器,它会在我们的认证和授权的过程中所抛出的异常进行捕获,并把这些异常进行handler回调操作,负责这个异常捕获的是 ExceptionTranslationFilter

 

认证过程的异常处理

一般情况下,我们使用Security做用户登陆的过程中,若查询数据库发现没有这个用户,或用户密码错误的时候,我们都会或手动,或Security自动抛出异常,这样做到的结果是,前端页面会报出 401 错误,但是这样的错误格式不是我们希望的统一封装返回结果格式,所以我们可以利用Security提供异常处理机制,对异常输出符合我们自己的返回格式。

认证的异常处理类为 AuthenticationEntryPoint

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        // 当用户不存在,或密码错误时,会回调认证异常处理方法
        WebUtil.renderJson(httpServletResponse, 401, "用户或密码错误", null);
    }
}

 

认证异常处理类,应注册在Security的异常处理方法配置中,覆盖默认的异常格式。

    @Resource
    private AuthenticationEntryPoint authenticationEntryPoint;
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
        
        return httpSecurity.build();
    }

 

 

授权过程的异常处理

对于已登陆用户访问不具有权限的url接口时,Security也会报出异常,但是这个异常如果不去自定义的话,也会像认证异常一样,前端报出 403 错误,为了自定义这种报错格式,我们也需要对访问没有权限的异常进行个性化输出。

负责授权异常处理的类为 AccessDeniedHandler

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        WebUtil.renderJson(httpServletResponse, HttpStatus.FORBIDDEN.value(), "用户权限不足", null);
    }
}

 

授权异常处理类,应注册在Security的异常处理方法配置中,覆盖默认的异常格式。

    @Resource
    private AccessDeniedHandler accessDeniedHandler;
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
        return httpSecurity.build();
    }

 

 

CORS 跨域

CORS 是同源跨域限制的问题,浏览器会对异步请求与当前网页网址不一致的地址会拦推禁止操作限制。不同源包括,域名不同、协议不同、端口不同,都会被认为是不同源请求。

如果是前后端分离项目,基本都会关闭跨域请求限制,要关闭跨域请求限制,我们需要在两个地方做关闭操作,一个是Security,一个是Spring MVC中的跨域限制

 

Spring MVC 中的关闭跨域限制:

@Configuration
public class WebMVCConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 可以跨域的url接口【全部】
                .allowCredentials(true) // 允许cookie
                .allowedHeaders("*") // 允许 header [全部]
                .allowedMethods("GET","POST","DELETE","PUT","OPTIONS") // 允许跨域的请求方法
                .allowedOrigins("*") // 允许请求的源网址域名 【任意域名】
                .maxAge(3600); // 允许跨域的最大请求时间
    }
}

 

Security 中的关闭跨域限制:

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {

        // 并闭 cors 跨域请求
        httpSecurity.cors().disable();

        return httpSecurity.build();
    }

 

登陆结果处理器

登陆结果处理器是由 UsernamePasswordAuthenticationFilter 过滤器提供的两种登陆状态回调方法,也就是说,它的生效只局限于使用 UsernamePasswordAuthenticationFilter 过滤器进行认证过滤时才有效。如果我们使用自定义的认证过滤器,则这些处理器都不会被触发。

在我们的默认 HttpSecurity 配置中,官方的默认代码中包含以下代码:

http.formLogin()

formLogin() 的内部会创建一个 UsernamePasswordAuthenticationFilter 过滤器加入到过滤器链中参与请求过滤的,所以如果我们自定义 HttpSecurity 配置中不使用 http.formLogin(). 则不会创建 UsernamePasswordAuthenticationFilter 过滤器。

 

 

登陆成功处理器

UsernamePasswordAuthenticationFilter 提供了认证成功后的处理器,可以对账户登陆成功后的一些处理工作,提供了接口  AuthenticationSuccessHandler

@Component
public class SuccessfulAuthHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        
    }
}

要使登陆成功处理器生效,需要把处理器加入到formLogin配置中。

 

    @Resource
    private AuthenticationSuccessHandler successfulAuthHandler;
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.formLogin().successHandler(successfulAuthHandler);
        return httpSecurity.build();
    }

 

 

 

 

登陆失败处理器

UsernamePasswordAuthenticationFilter 提供了认证失败后的处理器,可以对账户登陆失败后的一些处理工作,提供了接口  AuthenticationFailureHandler

@Component
public class FailFulAuthHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        
    }
}

要使登陆失败处理器生效,需要把处理器加入到formLogin配置中。

 

    @Resource
    private AuthenticationFailureHandler failFulAuthHandler;
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.formLogin().failureHandler(failFulAuthHandler);
        return httpSecurity.build();
    }

 

 

登出成功处理器

登出成功处理器由 UsernamePasswordAuthenticationFilter 过滤器提供,实现接口 LogoutSuccessHandler

@Component("successHandler")
public class SuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        
    }
}

要使登陆退出成功处理器生效,需要把处理器加入到formLogin配置中。

    @Resource
    private LogoutSuccessHandler successHandler;
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.logout().logoutSuccessHandler(successHandler);
        return httpSecurity.build();
    }

 

OAuth2

OAuth2 简介

OAuth2即【O】指Open,【Auth】指Authorization授权

OAuth2.0最简向导

 

OAuth2的角色

OAuth 2协议包含以下角色:

  1. 资源所有者(Resource Owner):即用户,资源的拥有人,想要通过客户应用访问资源服务器上的资源。

  2. 客户应用(Client):通常是一个Web或者无线应用,它需要访问用户的受保护资源。

  3. 资源服务器(Resource Server):存储受保护资源的服务器或定义了可以访问到资源的API,接收并验证客户端的访问令牌,以决定是否授权访问资源。

  4. 授权服务器(Authorization Server):负责验证资源所有者的身份并向客户端颁发访问令牌。

 

OAuth2 的使用场境

社交登录

在传统的身份验证中,用户需要提供用户名和密码,还有很多网站登录时,允许使用第三方网站的身份,这称为"第三方登录"。所谓第三方登录,实质就是 OAuth 授权。用户想要登录 A 网站,A 网站让用户提供第三方网站的数据,证明自己的身份。获取第三方网站的身份数据,就需要 OAuth 授权。

开放API

云冲印服务的实现为例

云冲印只提供冲印服务,但不提供照片存储服务,云冲印可以通过OAuth2 请求云存储服务获得照片数据,从而在不需要用户手动下载照片数据,再上传给云冲印了。

现代微服务安全

以微服务安全为例

微服务中,服务与服务之间可能需要做授权操作,特别是与第三方api服务请求进行授权访问时,使用OAuth2就比较方便了。

 

OAuth2的四种授权模式

相关知识:OAuth 2.0 的四种方式 - 阮一峰的网络日志 (ruanyifeng.com)

授权码(authorization-code)

授权码(authorization code),指的是第三方应用先申请一个授权码,然后再用该码获取令牌。

这种方式是最常用,最复杂,也是最安全的,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。

注册客户应用:客户应用如果想要访问资源服务器需要有凭证,需要在授权服务器上注册客户应用。注册后会**获取到一个ClientID和ClientSecrets

 

隐藏式

隐藏式(implicit),也叫简化模式,有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。

RFC 6749 规定了这种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为隐藏式。这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。

https://a.com/callback#token=ACCESS_TOKEN

将访问令牌包含在URL锚点中的好处:锚点在HTTP请求中不会发送到服务器,减少了泄漏令牌的风险。

 

密码式

密码式(Resource Owner Password Credentials):如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌。

这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。

凭证式

凭证式(client credentials):也叫客户端模式,适用于没有前端的命令行应用,即在命令行下请求令牌。

这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。

授权类型的选择

 

在Spring中搭建OAuth2

相关说明:OAuth2 :: Spring Security

Spring Security 包含

  • 客户应用(OAuth2 Client):OAuth2客户端功能中包含OAuth2 Login

  • 资源服务器(OAuth2 Resource Server)

Spring 不包含

  • 授权服务器(Spring Authorization Server):它是在Spring Security之上的一个单独的项目。

 

OAuth2 Spring 相关依赖

<!-- 资源服务器 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

<!-- 客户应用 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

<!-- 授权服务器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>

 

 

授权登录的实现思路

使用OAuth2 Login

Github OAuth2 创建应用实例

注册客户应用:

登录GitHub,在开发者设置中找到OAuth Apps,创建一个application,为客户应用创建访问GitHub的凭据:

 

填写应用信息:默认的重定向URI模板为{baseUrl}/login/oauth2/code/{registrationId} 。registrationId是ClientRegistration的唯一标识符。

 

获取应用程序id,生成应用程序密钥:

 

创建测试项目

创建一个springboot项目oauth2-login-demo,创建时引入如下依赖

示例代码参考:spring-security-samples/servlet/spring-boot/java/oauth2/login at 6.2.x · spring-projects/spring-security-samples (github.com)

配置OAuth客户端属性

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: 应用ID
            client-secret: 应用密钥
#            redirectUri: http://localhost:8200/login/oauth2/code/github

 

创建Controller

package com.atguigu.oauthdemo.controller;

@Controller
public class IndexController {

    @GetMapping("/")
    public String index(
            Model model,
            @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient,
            @AuthenticationPrincipal OAuth2User oauth2User) {
        model.addAttribute("userName", oauth2User.getName());
        model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName());
        model.addAttribute("userAttributes", oauth2User.getAttributes());
        return "index";
    }
}

 

 

创建html页面

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <title>Spring Security - OAuth 2.0 Login</title>
    <meta charset="utf-8" />
</head>
<body>
<div style="float: right" th:fragment="logout" sec:authorize="isAuthenticated()">
    <div style="float:left">
        <span style="font-weight:bold">User: </span><span sec:authentication="name"></span>
    </div>
    <div style="float:none">&nbsp;</div>
    <div style="float:right">
        <form action="#" th:action="@{/logout}" method="post">
            <input type="submit" value="Logout" />
        </form>
    </div>
</div>
<h1>OAuth 2.0 Login with Spring Security</h1>
<div>
    You are successfully logged in <span style="font-weight:bold" th:text="${userName}"></span>
    via the OAuth 2.0 Client <span style="font-weight:bold" th:text="${clientName}"></span>
</div>
<div>&nbsp;</div>
<div>
    <span style="font-weight:bold">User Attributes:</span>
    <ul>
        <li th:each="userAttribute : ${userAttributes}">
            <span style="font-weight:bold" th:text="${userAttribute.key}"></span>: <span th:text="${userAttribute.value}"></span>
        </li>
    </ul>
</div>
</body>
</html>

 

启动应用程序

  • 启动程序并访问localhost:8080。浏览器将被重定向到默认的自动生成的登录页面,该页面显示了一个用于GitHub登录的链接。

  • 点击GitHub链接,浏览器将被重定向到GitHub进行身份验证。

  • 使用GitHub账户凭据进行身份验证后,用户会看到授权页面,询问用户是否允许或拒绝客户应用访问GitHub上的用户数据。点击允许以授权OAuth客户端访问用户的基本个人资料信息。

  • 此时,OAuth客户端访问GitHub的获取用户信息的接口获取基本个人资料信息,并建立一个已认证的会话。

 

预设OAuth2配置类 CommonOAuth2Provider

CommonOAuth2Provider是一个预定义的通用OAuth2Provider,为一些知名资源服务API提供商(如Google、GitHub、Facebook)预定义了一组默认的属性。

例如,授权URI、令牌URI和用户信息URI通常不经常变化。因此,提供默认值以减少所需的配置。

因此,当我们配置GitHub客户端时,只需要提供client-id和client-secret属性。

GITHUB {
    public ClientRegistration.Builder getBuilder(String registrationId) {
        ClientRegistration.Builder builder = this.getBuilder(
        registrationId, 
        ClientAuthenticationMethod.CLIENT_SECRET_BASIC, 
        
        //授权回调地址(GitHub向客户应用发送回调请求,并携带授权码)   
		"{baseUrl}/{action}/oauth2/code/{registrationId}");
        builder.scope(new String[]{"read:user"});
        //授权页面
        builder.authorizationUri("https://github.com/login/oauth/authorize");
        //客户应用使用授权码,向 GitHub 请求令牌
        builder.tokenUri("https://github.com/login/oauth/access_token");
        //客户应用使用令牌向GitHub请求用户数据
        builder.userInfoUri("https://api.github.com/user");
        //username属性显示GitHub中获取的哪个属性的信息
        builder.userNameAttributeName("id");
        //登录页面超链接的文本
        builder.clientName("GitHub");
        return builder;
    }
},

如果您喜欢本站,点击这儿不花一分钱捐赠本站

这些信息可能会帮助到你: 下载帮助 | 报毒说明 | 进站必看

修改版本安卓软件,加群提示为修改者自留,非本站信息,注意鉴别

THE END
分享
二维码
打赏
海报
Spring Security 新理解
简介 本章节为SpringSecurity的二次深入了解   认证 认证是指用户登陆系统的登陆校验过程。 Spring Security 实际上是由多个拦截器(或叫过滤器)对用户访问的请求层层拦截和过滤,如下……
<<上一篇
下一篇>>