乐趣区

关于后端:项目实践一文带你搞定前后端分离下的认证和授权Spring-Security-JWT

前言

对于认证和受权,R 之前曾经写了两篇文章:

????【我的项目实际】在用平安框架前,我想先让你手撸一个登陆认证

????【我的项目实际】一文带你搞定页面权限、按钮权限以及数据权限

????【我的项目实际】2020 最新 Java 根底精讲视频教程和学习路线

在这两篇文章中咱们没有应用平安框架就搞定了认证和受权性能,并了解了其外围原理。R 在之前就说过,外围原理把握了,无论什么平安框架应用起来都会非常容易!那么本文就解说如何应用支流的平安框架 Spring Security 来实现认证和受权性能。

当然,本文并不只是对框架的应用办法进行解说,还会分析 Spring Security 的源码,看到最初你就会发现你把握了应用办法的同时,还对框架有了深度的了解!如果没有看过前两篇文章的,强烈建议先看一下,因为平安框架只是帮咱们封装了一些货色,背地的原理是不会变的。

本文所有代码都放在了 Github 上,克隆下来即可运行!

提纲挈领

Web 零碎中登录认证(Authentication)的外围就是 凭证 机制,无论是 Session 还是 JWT,都是在用户胜利登录时返回给用户一个凭证,后续用户拜访接口需携带凭证来表明本人的身份。后端会对须要进行认证的接口进行平安判断,若凭证没问题则代表已登录就放行接口,若凭证有问题则间接拒绝请求。这个平安判断 都是放在过滤器里对立解决的

登录认证是对 用户的身份 进行确认,权限受权(Authorization)是对 用户是否拜访某个资源 进行确认,受权产生都认证之后。认证一样,这种通用逻辑都是放在过滤器里进行的对立操作:

LoginFilter先进行登录认证判断,认证通过后再由 AuthFilter 进行权限受权判断,一层一层没问题后才会执行咱们真正的业务逻辑。

Spring Security 对 Web 零碎的反对 就是基于这一个个过滤器组成的过滤器链

用户申请都会通过 Servlet 的过滤器链,在之前两篇文章中咱们就是通过自定义的两个过滤器实现了认证受权性能!而 Spring Security 也是做的同样的事实现了一系列性能:

Servlet 过滤器链中,Spring Security 向其增加了一个 FilterChainProxy 过滤器,这个代理过滤器会创立一套 Spring Security 自定义的过滤器链,而后执行一系列过滤器。咱们能够大略看一下 FilterChainProxy 的大抵源码:

@Override
public void doFilter(ServletRequest request, ServletResponse response,
                     FilterChain chain) throws IOException, ServletException {
    ... 省略其余代码
    
    // 获取 Spring Security 的一套过滤器
    List<Filter> filters = getFilters(request);
    // 将这一套过滤器组成 Spring Security 本人的过滤链,并开始执行
    VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
    vfc.doFilter(request, response);
    
    ... 省略其余代码
}
复制代码

咱们能够看一下 Spring Security 默认会启用多少过滤器:

这外面咱们只须要重点关注两个过滤器即可:UsernamePasswordAuthenticationFilter负责登录认证,FilterSecurityInterceptor负责权限受权。

????Spring Security 的外围逻辑全在这一套过滤器中,过滤器里会调用各种组件实现性能,把握了这些过滤器和组件你就把握了 Spring Security!这个框架的应用形式就是对这些过滤器和组件进行扩大。

肯定要记住这句话,带着这句话去应用和了解 Spring Security,你会像站在高处鸟瞰,整个框架的脉络高深莫测。

方才咱们总览了一下全局,当初咱们就开始进行代码编写了。

要应用 Spring Security 必定是要先引入依赖包(Web 我的项目其余必备依赖我在之前文章中已解说,这里就不过多论述了):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
复制代码

依赖包导入后,Spring Security 就默认提供了许多性能将整个利用给爱护了起来:

???? 要求通过身份验证的用户能力与应用程序进行交互

???? 创立好了默认登录表单

???? 生成用户名为 user 的随机明码并打印在管制台上

????CSRF攻打防护、Session Fixation攻打防护

???? 等等等等 ……

在理论开发中,这些 默认配置好的性能往往不合乎咱们的理论需要,所以咱们个别会自定义一些配置。配置形式很简略,新建一个配置类即可:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
复制代码

在该类中重写 WebSecurityConfigurerAdapter 的办法就能对 Spring Security 进行自定义配置。

登录认证

依赖包和配置类筹备好后,接下来咱们要实现的第一个性能那天然是登录认证,毕竟用户要应用咱们零碎第一步就是登录。之前文章介绍了 SessionJWT两种认证形式,这里咱们来用 Spring Security 实现这两种认证。

最简略的认证形式

不论哪种认证形式和框架,有些外围概念是不会变的,这些外围概念在平安框架中会以各种组件来体现,理解各个组件的同时性能也就跟着实现了性能。

咱们零碎中会有许多用户,确认以后是哪个用户正在应用咱们零碎就是登录认证的最终目标。这里咱们就提取出了一个外围概念:以后登录用户 / 以后认证用户。整个系统安全都是围绕以后登录用户开展的!这个不难理解,要是以后登录用户都不能确认了,那 A 下了一个订单,下到了 B 的账户上这不就乱套了。这一概念在 Spring Security 中的体现就是 ????Authentication,它存储了认证信息,代表以后登录用户。

咱们在程序中如何获取并应用它呢?咱们须要通过 ????SecurityContext 来获取 Authentication,看了之前文章的敌人大略就猜到了这个SecurityContext 就是咱们的上下文对象!

这种在一个线程中横跨若干办法调用,须要传递的对象,咱们通常称之为上下文(Context)。上下文对象是十分有必要的,否则你每个办法都得额定减少一个参数接管对象,切实太麻烦了。

这个上下文对象则是交由 ????SecurityContextHolder 进行治理,你能够在程序 任何中央 应用它:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
复制代码

能够看到调用链路是这样的:SecurityContextHolder????SecurityContext????Authentication

SecurityContextHolder原理非常简单,就是和咱们之前实现的上下文对象一样,应用 ThreadLocal 来保障一个线程中传递同一个对象!源码我就不贴了,具体可看之前文章写的上下文对象实现。

当初咱们曾经晓得了 Spring Security 中三个外围组件:

????Authentication:存储了认证信息,代表以后登录用户

????SeucirtyContext:上下文对象,用来获取Authentication

????SecurityContextHolder:上下文治理对象,用来在程序任何中央获取SecurityContext

他们关系如下:


Authentication中那三个玩意就是认证信息:

????Principal:用户信息,没有认证时个别是用户名,认证后个别是用户对象

????Credentials:用户凭证,个别是明码

????Authorities:用户权限

当初咱们晓得如何获取并应用以后登录用户了,那这个用户是怎么进行认证的呢?总不能我轻易 new 一个就代表用户认证结束了吧。所以咱们还缺一个生成 Authentication 对象的认证过程!

认证过程就是登录过程,不应用平安框架时咱们的认证过程是这样的:

查问用户数据???? 判断账号密码是否正确???? 正确则将用户信息存储到上下文中???? 上下文中有了这个对象则代表该用户登录了

Spring Security 的认证流程也是如此:

Authentication authentication = new UsernamePasswordAuthenticationToken(用户名, 用户明码, 用户的权限汇合);
SecurityContextHolder.getContext().setAuthentication(authentication);
复制代码

和不应用平安框架一样,将认证信息放到上下文中就代表用户已登录。下面代码演示的就是 Spring Security 最简略的认证形式,间接将 Authentication 搁置到 SecurityContext 中就实现认证了!

这个流程和之前获取以后登录用户的流程天然是相同的:Authentication????SecurityContext????SecurityContextHolder

是不是感觉,就这?这就实现认证啦?这也太简略了吧。对于 Spring Security 来说,这样的确就实现了认证,但对于咱们来说还少了一步,那就是判断用户的账号密码是否正确。用户进行登录操作时从会传递过去账号密码,咱们必定是要查问用户数据而后判断传递过去的账号密码是否正确,只有正确了咱们才会将认证信息放到上下文对象中,不正确就间接提醒谬误:

// 调用 service 层执行判断业务逻辑
if (!userService.login(用户名, 用户明码)) {return "账号密码谬误";}
// 账号密码正确了才将认证信息放到上下文中(用户权限须要再从数据库中获取,前面再说,这里省略)Authentication authentication = new UsernamePasswordAuthenticationToken(用户名, 用户明码, 用户的权限汇合);
SecurityContextHolder.getContext().setAuthentication(authentication);
复制代码

这样才算是一个残缺的认证过程,和不应用平安框架时的流程是一样的哦,只是一些组件之前是咱们本人实现的。

这里查问用户信息并校验账号密码是齐全由咱们本人在业务层编写所有逻辑,其实这一块 Spring Security 也有组件供咱们应用:

AuthenticationManager 认证形式

????AuthenticationManager 就是 Spring Security 用于执行身份验证的组件,只须要调用它的 authenticate 办法即可实现认证。Spring Security 默认的认证形式就是在 UsernamePasswordAuthenticationFilter 这个过滤器中调用这个组件,该过滤器负责认证逻辑。

咱们要依照本人的形式应用这个组件,先在之前配置类配置一下:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {return super.authenticationManager();
    }
}
复制代码

这里咱们写上残缺的登录接口代码:

@RestController
@RequestMapping("/API")
public class LoginController {
    @Autowired
    private AuthenticationManager authenticationManager;

    @PostMapping("/login")
    public String login(@RequestBody LoginParam param) {
        // 生成一个蕴含账号密码的认证信息
        Authentication token = new UsernamePasswordAuthenticationToken(param.getUsername(), param.getPassword());
        // AuthenticationManager 校验这个认证信息,返回一个已认证的 Authentication
        Authentication authentication = authenticationManager.authenticate(token);
        // 将返回的 Authentication 存到上下文中
        SecurityContextHolder.getContext().setAuthentication(authentication);
        return "登录胜利";
    }
}
复制代码

留神,这里流程和之前说的流程是齐全一样的,只是用户身份验证改成了应用 AuthenticationManager 来进行。

AuthenticationManager的校验逻辑非常简单:

依据用户名先查问出用户对象(没有查到则抛出异样)???? 将用户对象的明码和传递过去的明码进行校验,明码不匹配则抛出异样

这个逻辑没啥好说的,再简略不过了。重点是这里每一个步骤 Spring Security 都提供了组件:

???? 是谁执行 依据用户名查问出用户对象 逻辑的呢?用户对象数据能够存在内存中、文件中、数据库中,你得确定好怎么查才行。这一部分就是交由????UserDetialsService 解决,该接口只有一个办法loadUserByUsername(String username),通过用户名查问用户对象,默认实现是在内存中查问。

???? 那查问进去的 用户对象 又是什么呢?每个零碎中的用户对象数据都不尽相同,咱们须要确认咱们的用户数据是啥样的才行。Spring Security 中的用户数据则是由????UserDetails 来体现,该接口中提供了账号、明码等通用属性。

????对明码进行校验 大家可能会感觉比较简单,if、else搞定,就没必要用什么组件了吧?但框架毕竟是框架思考的比拟周全,除了 if、else 外还解决了明码加密的问题,这个组件就是????PasswordEncoder,负责明码加密与校验。

咱们能够看下 AuthenticationManager 校验逻辑的大略源码:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    ... 省略其余代码
    
    // 传递过去的用户名
    String username = authentication.getName();
    // 调用 UserDetailService 的办法,通过用户名查问出用户对象 UserDetail(查问不进去 UserDetailService 则会抛出异样)UserDetails userDetails = this.getUserDetailsService().loadUserByUsername(username);
    String presentedPassword = authentication.getCredentials().toString();
    
    // 传递过去的明码
    String password = authentication.getCredentials().toString();
    // 应用明码解析器 PasswordEncoder 传递过去的明码是否和实在的用户明码匹配
    if (!passwordEncoder.matches(password, userDetails.getPassword())) {
        // 明码谬误则抛出异样
        throw new BadCredentialsException("错误信息...");
    }
    
    // 留神哦,这里返回的已认证 Authentication,是将整个 UserDetails 放进去充当 Principal
    UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userDetails,
                authentication.getCredentials(), userDetails.getAuthorities());
    return result;
    
    ... 省略其余代码
}
复制代码

UserDetialsService????UserDetails????PasswordEncoder,这三个组件 Spring Security 都有默认实现,这个别是满足不了咱们的理论需要的,所以这里咱们本人来实现这些组件!

加密器 PasswordEncoder

首先是PasswordEncoder,这个接口很简略就两个重要办法:

public interface PasswordEncoder {
    /**
      * 加密
      */
    String encode(CharSequence rawPassword);
    /**
      * 将未加密的字符串(前端传递过去的明码)和已加密的字符串(数据库中存储的明码)进行校验
      */
    boolean matches(CharSequence rawPassword, String encodedPassword);
}
复制代码

你能够实现此接口定义本人的加密规定和校验规定,不过 Spring Security 提供了很多加密器实现,咱们这里选定一个就好。能够在之前所说的 配置类 里进行如下配置:

@Bean
public PasswordEncoder passwordEncoder() {
    // 这里咱们应用 bcrypt 加密算法,安全性比拟高
    return new BCryptPasswordEncoder();}
复制代码

因为明码加密是我后面文章多数没有介绍的性能,所以这里额定提一嘴。往数据库中增加用户数据时就要将明码进行加密,否则后续进行明码校验时从数据库拿进去的还是明文明码,是无奈通过校验的。比方咱们有一个用户注册的接口:

@Autowired
private PasswordEncoder passwordEncoder;

@PostMapping("/register")
public String register(@RequestBody UserParam param) {UserEntity user = new UserEntity();
    // 调用加密器将前端传递过去的明码进行加密
    user.setUsername(param.getUsername()).setPassword(passwordEncoder.encode(param.getPassword()));
    // 将用户实体对象增加到数据库
    userService.save(user);
    return "注册胜利";
}
复制代码

这样数据库中存储的明码都是已加密的了:

用户对象 UserDetails

该接口就是咱们所说的用户对象,它提供了用户的一些通用属性:

public interface UserDetails extends Serializable {
   /**
    * 用户权限汇合(这个权限对象当初不论它,到权限时我会解说)*/
   Collection<? extends GrantedAuthority> getAuthorities();
   /**
    * 用户明码
    */
   String getPassword();
   /**
    * 用户名
    */
   String getUsername();
   /**
    * 用户没过期返回 true,反之则 false
    */
   boolean isAccountNonExpired();
   /**
    * 用户没锁定返回 true,反之则 false
    */
   boolean isAccountNonLocked();
   /**
    * 用户凭据 (通常为明码) 没过期返回 true,反之则 false
    */
   boolean isCredentialsNonExpired();
   /**
    * 用户是启用状态返回 true,反之则 false
    */
   boolean isEnabled();}
复制代码

理论开发中咱们的用户属性各种各样,这些默认属性必然是满足不了,所以咱们个别会本人实现该接口,而后设置好咱们理论的用户实体对象。实现此接口要重写很多办法比拟麻烦,咱们能够继承 Spring Security 提供的 org.springframework.security.core.userdetails.User 类,该类实现了 UserDetails 接口帮咱们省去了重写办法的工作:

public class UserDetail extends User {
    /**
     * 咱们本人的用户实体对象,要调取用户信息时间接获取这个实体对象。(这里我就不写 get/set 办法了)*/
    private UserEntity userEntity;

    public UserDetail(UserEntity userEntity, Collection<? extends GrantedAuthority> authorities) {
        // 必须调用父类的构造方法,以初始化用户名、明码、权限
        super(userEntity.getUsername(), userEntity.getPassword(), authorities);
        this.userEntity = userEntity;
    }
}
复制代码

业务对象 UserDetailsService

该接口很简略只有一个办法:

public interface UserDetailsService {
    /**
     * 依据用户名获取用户对象(获取不到间接抛异样)*/
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
复制代码

咱们本人的用户业务类该接口即可实现本人的逻辑:

@Service
public class UserServiceImpl implements UserService, UserDetailsService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) {
        // 从数据库中查问出用户实体对象
        UserEntity user = userMapper.selectByUsername(username);
        // 若没查问到肯定要抛出该异样,这样能力被 Spring Security 的谬误处理器解决
        if (user == null) {throw new UsernameNotFoundException("没有找到该用户");
        }
        // 走到这代表查问到了实体对象,那就返回咱们自定义的 UserDetail 对象(这里权限临时放个空集合,前面我会解说)return new UserDetail(user, Collections.emptyList());
    }
}
复制代码

AuthenticationManager校验所调用的三个组件咱们就曾经做好实现了!

不晓得大家留神到没有,当咱们查问用户失败时或者校验明码失败时都会抛出 Spring Security 的自定义异样。这些异样不可能放任不管,Spring Security 对于这些异样都是在 ExceptionTranslationFilter 过滤器中进行解决(能够回顾一下后面的过滤器截图),而????AuthenticationEntryPoint 则专门解决认证异样!

认证异样处理器 AuthenticationEntryPoint

该接口也只有一个办法:

public interface AuthenticationEntryPoint {
    /**
     * 接管异样并解决
     */
    void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException);
}
复制代码

咱们来自定义一个类实现咱们本人的错误处理逻辑:

public class MyEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        // 间接提醒前端认证谬误
        out.write("认证谬误");
        out.flush();
        out.close();}
}
复制代码

用户传递过去账号密码???? 认证校验???? 异样解决,这一整套流程的组件咱们就都给定义完了!当初只差最初一步,就是在 Spring Security 配置类外面进行一些配置,能力让这些失效。

配置

Spring Security 对哪些接口进行爱护、什么组件失效、某些性能是否启用等等都须要在配置类中进行配置,留神看代码正文:

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserServiceImpl userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 敞开 csrf 和 frameOptions,如果不敞开会影响前端申请接口(这里不开展细讲了,感兴趣的自行理解)http.csrf().disable();
        http.headers().frameOptions().disable();
        // 开启跨域以便前端调用接口
        http.cors();

        // 这是配置的要害,决定哪些接口开启防护,哪些接口绕过防护
        http.authorizeRequests()
                // 留神这里,是容许前端跨域联调的一个必要配置
                .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
                // 指定某些接口不须要通过验证即可拜访。登陆、注册接口必定是不须要认证的
                .antMatchers("/API/login", "/API/register").permitAll()
                // 这里意思是其它所有接口须要认证能力拜访
                .anyRequest().authenticated()
                // 指定认证谬误处理器
                .and().exceptionHandling().authenticationEntryPoint(new MyEntryPoint());
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 指定 UserDetailService 和加密器
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {return super.authenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();
    }
}
复制代码

其中用的最多的就是 configure(HttpSecurity http) 办法,能够通过HttpSecurity 进行许多配置。当咱们重写这个办法时,就曾经敞开了默认的表单登录形式,而后咱们再配置好启用哪些组件、指定哪些接口须要认证,就搞定了!

假如当初咱们有一个 /API/test 接口,在没有登录的时候调用该接口看下成果:

咱们登录一下:

而后再调用测试接口:

能够看到未登录时测试接口是无奈失常拜访的,会依照咱们在 EntryPoint 中的逻辑返回谬误提醒。

总结和补充

有人可能会问,用 AuthenticationManager 认证形式要配置好多货色啊,我就用之前说的那种最简略的形式不行吗?当然是能够的啦,用哪种形式都轻易,只有实现性能都行。其实不论哪种形式咱们的认证的逻辑代码一样都没少,只不过一个是咱们本人业务类全副搞定,一个是能够集成框架的组件。这里也顺带再总结一下流程:

  1. 用户调进行登录操作,传递账号密码过去???? 登录接口调用AuthenticationManager
  2. 依据用户名查问出用户数据????UserDetailService查问出UserDetails
  3. 将传递过去的明码和数据库中的明码进行比照校验????PasswordEncoder
  4. 校验通过则将认证信息存入到上下文中???? 将 UserDetails 存入到 Authentication,将Authentication 存入到SecurityContext
  5. 如果认证失败则抛出异样???? 由 AuthenticationEntryPoint 解决

方才咱们讲的认证形式都是基于 session 机制,认证后 Spring Security 会将 Authentication 存入到 session 中,Key 为HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY。也就是说,你齐全能够通过如下形式获取Authentication

Authentication = (Authentication)session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY)
复制代码

当然,官网还是不举荐这样间接操作的,因为对立通过 SecurityContextHolder 操作更利于治理!应用 SecurityContextHolder 除了获取以后用户外,退出登录的操作也是很不便的:

@GetMapping("/logout")
public String logout() {SecurityContextHolder.clearContext();
    return "退出胜利";
}
复制代码

session认证咱们就解说到此,接下来咱们解说 JWT 的认证。

JWT 集成

对于 JWT 的介绍和工具类等我在后面文章曾经讲的很分明了,这里我就不额定阐明了,间接带大家实现代码。

采纳 JWT 的形式进行认证首先做的第一步就是在配置类里禁用掉session

// 禁用 session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
复制代码

留神,这里的禁用是指 Spring Security 不采纳 session 机制了,不代表你禁用掉了整个零碎的 session 性能。

而后咱们再批改一下登录接口,当用户登录胜利的同时,咱们须要生成 token 并返回给前端,这样前端能力拜访其余接口时携带token

@Autowired
private UserService userService;

@PostMapping("/login")
public UserVO login(@RequestBody @Validated LoginParam user) {
    // 调用业务层执行登录操作
    return userService.login(user);
}
复制代码

业务层办法:

public UserVO login(LoginParam param) {
    // 依据用户名查问出用户实体对象
    UserEntity user = userMapper.selectByUsername(param.getUsername());
    // 若没有查到用户 或者 明码校验失败则抛出自定义异样
    if (user == null || !passwordEncoder.matches(param.getPassword(), user.getPassword())) {throw new ApiException("账号密码谬误");
    }

    // 须要返回给前端的 VO 对象
    UserVO userVO = new UserVO();
    userVO.setId(user.getId())
        .setUsername(user.getUsername())
        // 生成 JWT,将用户名数据存入其中
        .setToken(jwtManager.generate(user.getUsername()));
    return userVO;
}
复制代码

咱们执行一下登录操作:

咱们能够看到登录胜利时接口会返回 token,后续咱们再拜访其它接口时须要将token 放到申请头中。这里咱们须要自定义一个认证过滤器,来对 token 进行校验:

@Component
public class LoginFilter extends OncePerRequestFilter {
    @Autowired
    private JwtManager jwtManager;
    @Autowired
    private UserServiceImpl userService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        // 从申请头中获取 token 字符串并解析(JwtManager 之前文章有详解,这里不多说了)Claims claims = jwtManager.parse(request.getHeader("Authorization"));
        if (claims != null) {
            // 从 `JWT` 中提取出之前存储好的用户名
            String username = claims.getSubject();
            // 查问出用户对象
            UserDetails user = userService.loadUserByUsername(username);
            // 手动组装一个认证对象
            Authentication authentication = new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
            // 将认证对象放到上下文中
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }
}
复制代码

过滤器中的逻辑和之前介绍的 最简略的认证形式 逻辑是统一的,每当一个申请来时咱们都会校验 JWT 进行认证,上下文对象中有了 Authentication 后续过滤器就会晓得该申请曾经认证过了。

咱们这个自定义的过滤器须要替换掉 Spring Security 默认的认证过滤器,这样咱们的过滤器能力失效,所以咱们须要进行如下配置:

// 将咱们自定义的认证过滤器替换掉默认的认证过滤器
http.addFilterBefore(loginFilter, UsernamePasswordAuthenticationFilter.class);
复制代码

咱们能够断点调试看一下当初的过滤器是怎么的:

能够看到咱们自定义的过滤器曾经替换掉了 UsernamePasswordAuthenticationFilter 默认过滤器了!当咱们携带 token 拜访接口时能够发现曾经失效:

登录认证到此就解说结束了,接下来咱们一鼓作气来实现权限受权!

权限受权

菜单权限次要是通过前端渲染,数据权限次要靠 SQL 拦挡,和 Spring Security 没太大耦合,就不多开展了。咱们来梳理一下 接口权限 的受权的流程:

  1. 当一个申请过去,咱们先得晓得这个申请的规定,即须要怎么的权限能力拜访
  2. 而后获取以后登录用户所领有的权限
  3. 再校验以后用户是否领有该申请的权限
  4. 用户领有这个权限则失常返回数据,没有权限则拒绝请求

实现了登录认证性能后,想必大家曾经有点感觉:Spring Security 将流程性能分得很细,每一个小性能都会有一个组件专门去做,咱们要做的就是去自定义这些组件!Spring Security 针对上述流程也提供了许多组件。

Spring Security 的受权产生在 FilterSecurityInterceptor 过滤器中:

  1. 首先调用的是????SecurityMetadataSource,来获取以后申请的鉴权规定
  2. 而后通过 Authentication 获取以后登录用户所有权限数据:????GrantedAuthority,这个咱们后面提过,认证对象里寄存这权限数据
  3. 再调用????AccessDecisionManager 来校验以后用户是否领有该权限
  4. 如果有就放行接口,没有则抛出异样,该异样会被????AccessDeniedHandler 解决

咱们能够来看一下过滤器里大略的源码:

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    ... 省略其它代码
        
    // 这是 Spring Security 封装的对象,该对象里蕴含了 request 等信息
    FilterInvocation fi = new FilterInvocation(request, response, chain);
    // 这里调用了父类的 AbstractSecurityInterceptor 的办法, 认证外围逻辑根本全在父类里
    InterceptorStatusToken token = super.beforeInvocation(fi);

    ... 省略其它代码
}
复制代码

父类的 beforeInvocation 大略源码如下:

protected InterceptorStatusToken beforeInvocation(Object object) {
    ... 省略其它代码
    
    // 调用 SecurityMetadataSource 来获取以后申请的鉴权规定,这个 ConfigAttribue 就是规定,前面我会讲
    Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
    // 如果以后申请啥规定也没有,就代表该申请无需受权即可拜访,间接完结办法
    if (CollectionUtils.isEmpty(attributes)) {return null;}
    
    // 获取以后登录用户
    Authentication authenticated = authenticateIfRequired();
    // 调用 AccessDecisionManager 来校验以后用户是否领有该权限,没有权限则抛出异样
    this.accessDecisionManager.decide(authenticated, object, attributes);
    
    ... 省略其它代码
}
复制代码

陈词滥调,外围流程都是一样的。咱们接下来自定义这些组件,以实现咱们本人的鉴权逻辑。

鉴权规定源 SecurityMetadataSource

该接口咱们只须要关注一个办法:

public interface SecurityMetadataSource {
    /**
     * 获取以后申请的鉴权规定
     
     * @param object 该参数就是 Spring Security 封装的 FilterInvocation 对象,蕴含了很多 request 信息
     * @return 鉴权规定对象
     */
    Collection<ConfigAttribute> getAttributes(Object object);

}
复制代码

ConfigAttribute就是咱们所说的鉴权规定,该接口只有一个办法:

public interface ConfigAttribute {
    /**
     * 这个字符串就是规定,它能够是角色名、权限名、表达式等等。* 你齐全能够依照本人想法来定义,前面 AccessDecisionManager 会用这个字符串
     */
    String getAttribute();}
复制代码

在之前文章中咱们受权的实现全是靠着资源 id,用户id 关联角色 id,角色id 关联资源id,这样用户就相当于关联了资源,而咱们接口资源在数据库中的体现是这样的:

这里还是一样,咱们照样以资源 id 作为权限的标记。接下咱们就来自定义 SecurityMetadataSource 组件:

@Component
public class MySecurityMetadataSource implements SecurityMetadataSource {
    /**
     * 以后零碎所有接口资源对象,放在这里相当于一个缓存的性能。* 你能够在利用启动时将该缓存给初始化,也能够在应用过程中加载数据,这里我就不多开展阐明了
     */
    private static final Set<Resource> RESOURCES = new HashSet<>();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) {
        // 该对象是 Spring Security 帮咱们封装好的,能够通过该对象获取 request 等信息
        FilterInvocation filterInvocation = (FilterInvocation) object;
        HttpServletRequest request = filterInvocation.getRequest();
        // 遍历所有权限资源,以和以后申请进行匹配
        for (Resource resource : RESOURCES) {// 因为咱们 url 资源是这种格局:GET:/API/user/test/{id},冒号后面是申请办法,冒号前面是申请门路,所以要字符串拆分
            String[] split = resource.getPath().split(":");
            // 因为 /API/user/test/{id}这种门路参数不能间接 equals 来判断申请门路是否匹配,所以须要用 Ant 类来匹配
            AntPathRequestMatcher ant = new AntPathRequestMatcher(split[1]);
            // 如果申请办法和申请门路都匹配上了,则代表找到了这个申请所需的权限资源
            if (request.getMethod().equals(split[0]) && ant.matches(request)) {
                // 将咱们权限资源 id 返回,这个 SecurityConfig 就是 ConfigAttribute 一个简略实现
                return Collections.singletonList(new SecurityConfig(resource.getId().toString()));
            }
        }
        // 走到这里就代表该申请无需受权即可拜访,返回空
        return null;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        // 不必管,这么写就行
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        // 不必管,这么写就行
        return true;
    }
}
复制代码

留神,咱们这里返回的 ConfigAttribute 鉴权规定,就是咱们的资源id

用户权限 GrantedAuthority

该组件代表用户所领有的权限,和 ConfigAttribute 一样也只有一个办法,该办法返回的字符串就是代表着权限

public interface GrantedAuthority extends Serializable {String getAuthority();
}
复制代码

GrantedAuthorityConfigAttribute一比照,就晓得用户是否领有某个权限了。

Spring Security 对 GrantedAuthority 有一个简略实现 SimpleGrantedAuthority,对咱们来说够用了,所以咱们额定再新建一个实现。咱们要做的就是在UserDetialsService 中,获取用户对象的同时也将权限数据查问进去:

@Override
public UserDetails loadUserByUsername(String username) {UserEntity user = userMapper.selectByUsername(username);
    if (user == null) {throw new UsernameNotFoundException("没有找到该用户");
    }
    // 先将该用户所领有的资源 id 全副查问进去,再转换成 `SimpleGrantedAuthority` 权限对象
    Set<SimpleGrantedAuthority> authorities = resourceService.getIdsByUserId(user.getId())
        .stream()
        .map(String::valueOf)
        .map(SimpleGrantedAuthority::new)
        .collect(Collectors.toSet());
    // 将用户实体和权限汇合都放到 UserDetail 中,return new UserDetail(user, authorities);
}
复制代码

这样当认证结束时,Authentication就会领有用户信息和权限数据了。

受权治理 AccessDecisionManager

终于要来到咱们真正的受权组件了,这个组件才最终决定了你有没有某个权限,该接口咱们只需关注一个办法:

public interface AccessDecisionManager {

    /**
     * 受权操作,如果没有权限则抛出异样 
     *
     * @param authentication 以后登录用户,以获取以后用户权限信息
     * @param object FilterInvocation 对象,以获取 request 信息
     * @param configAttributes 以后申请鉴权规定
     */
    void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
            throws AccessDeniedException, InsufficientAuthenticationException;
}
复制代码

该办法承受了这几个参数后齐全能做到权限校验了,咱们来实现本人的逻辑:

@Component
public class MyDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) {
        // 如果受权规定为空则代表此 URL 无需受权就能拜访
        if (Collections.isEmpty(configAttributes)) {return;}
        // 判断受权规定和以后用户所属权限是否匹配
        for (ConfigAttribute ca : configAttributes) {for (GrantedAuthority authority : authentication.getAuthorities()) {
                // 如果匹配上了,代表以后登录用户是有该权限的,间接完结办法
                if (Objects.equals(authority.getAuthority(), ca.getAttribute())) {return;}
            }
        }
        // 走到这里就代表没有权限,必须要抛出异样,否则谬误处理器捕获不到
        throw new AccessDeniedException("没有相干权限");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        // 不必管,这么写就行
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        // 不必管,这么写就行
        return true;
    }
}
复制代码

受权谬误处理器 AccessDeniedHandler

该组件和之前的认证异样处理器一样,只有一个办法用来解决异样,只不过这个是用来解决受权异样的。咱们间接来实现:

public class MyDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {response.setContentType("application/json;charset=utf-8");
        out.write("没有相干权限");
        out.flush();
        out.close();}
}
复制代码

配置

组件都定义好了,那咱们接下来就是最初一步咯,就是让这些组件失效。咱们的鉴权规定源组件 SecurityMetadataSource 和受权治理组件 AccessDecisionManager 必须通过受权过滤器 FilterSecurityInterceptor 来配置失效,所以咱们得本人先写一个过滤器,这个过滤器的外围代码根本依照父类的写就行,次要就是属性的配置:

@Component
public class AuthFilter extends AbstractSecurityInterceptor implements Filter {
    @Autowired
    private SecurityMetadataSource securityMetadataSource;

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        // 将咱们自定义的 SecurityMetadataSource 给返回
        return this.securityMetadataSource;
    }

    @Override
    @Autowired
    public void setAccessDecisionManager(AccessDecisionManager accessDecisionManager) {
        // 将咱们自定义的 AccessDecisionManager 给注入
        super.setAccessDecisionManager(accessDecisionManager);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 上面的就是依照父类写法写的
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
            // 执行下一个拦截器
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        }  finally {
            // 申请之后的解决
            super.afterInvocation(token, null);
        }
    }

    @Override
    public Class<?> getSecureObjectClass() {return FilterInvocation.class;}

    @Override
    public void init(FilterConfig filterConfig) {}

    @Override
    public void destroy() {}
}

复制代码

过滤器定义好了,咱们回到 Spring Security 配置类让这个过滤器替换掉原有的过滤器就所有都搞定啦:

http.addFilterBefore(authFilter, FilterSecurityInterceptor.class);
复制代码

咱们能够来看下成果,没有权限的状况下拜访接口:

有权限的状况下拜访接口:

总结

整个 Spring Security 就解说结束了,咱们对两个过滤器、N 多个组件进行了自定义实现,从而达到了咱们的性能。这里做了一个思维导图不便大家了解:

别看组件这么多,认证受权的外围流程和一些概念是不会变的,什么平安框架都万变不离其宗。比方 Shiro,其中最根本的概念Subject 就代表以后用户,SubjectManager就是用户管理器……

在我前两篇文章中有人也谈到用平安框架还不如本人手写,的确,手写能够最大灵便度依照本人的想法来(并且也不简单),应用平安框架反而要配合框架的定式,如同被解放了。那平安框架比照手写有什么劣势呢?我感觉劣势次要有如下两点:

  1. 一些性能开箱即用,比方 Spring Security 的加密器,十分不便
  2. 框架的定式既是解放也是标准,无论谁接手你的我的项目,一看到相熟的平安框架就能立马上手

解说到这里就完结了,本文所有代码、SQL语句都放在 Github,克隆下来即可运行。

原文链接:https://juejin.cn/post/690072…

退出移动版