Spring Security系列文章

  • 认证与授权之Cookie、Session、Token、JWT
  • 依据Session的认证与授权实践
  • Spring Security入门学习
  • Spring Security进阶学习
  • Spring Security自定义认证逻辑完结图片验证码登录
  • Spring Security结合JWT完结认证与授权

经过这段时刻的学习,咱们已不满足于一般事例的实操,想着啥时分找个更贴合实践运用的事例练练手,比方说 SpringSecurity+JWT,正好将近期的知识点糅合起来,也算是实操了依据 Token 的认证与授权。

闲话少说,咱们直奔主题,开端本次事例的学习。

SpringSecurity认证与授权

自定义认证处理

还记得上一篇文章中说到的验证码处理逻辑吗?咱们经过自定义 DaoAuthenticationProvider 完结类,并重写 additionalAuthenticationChecks 办法,将验证码比对逻辑和密码比对逻辑放在一同。但假如只是为了校验验证码,还有一种完结办法:能够自定义一个过滤器,并把这个自定义的过滤器放入 SpringSecurity 过滤器链中,每次恳求都会经过该过滤器。但该办法存在一个弊端,咱们只期望登录恳求经过该过滤器即可,其他恳求是不需求经过该过滤器的

Spring Security 的默许 Filter 链:

 SecurityContextPersistenceFilter
->HeaderWriterFilter
->LogoutFilter
->UsernamePasswordAuthenticationFilter
->RequestCacheAwareFilter
->SecurityContextHolderAwareRequestFilter
->SessionManagementFilter
->ExceptionTranslationFilter
->FilterSecurityInterceptor

这些过滤器按照既定的优先级摆放,最终构成一个过滤器链,如下图所示。开发人员也能够自定义过滤器,并经过 @Order 注解来调整自定义过滤器在过滤器链中的方位。

Spring Security结合JWT实现认证与授权

下面咱们要点关注 UsernamePasswordAuthenticationFilter,该过滤器用于处理依据表单办法的登录验证,该过滤器默许只有当恳求办法为post、恳求页面为/login时过滤器才生效,假如想修改默许阻拦url,只需在刚才介绍的Spring Security装备类WebSecurityConfig中装备该过滤器的阻拦url:.loginProcessingUrl(“url”)即可;

当用户发送登录恳求的时分,首要进入到 UsernamePasswordAuthenticationFilter 中进行校验。

Spring Security结合JWT实现认证与授权

打断点发送登录恳求进入源码中,咱们会发现它会进入到 UsernamePasswordAuthenticationFilter,在该类中,有一个attemptAuthentication办法在这个办法中,会获取恳求传入的 username 以及 password 参数的信息,然后运用结构器 new UsernamePasswordAuthenticationToken(username, password)封装为一个 UsernamePasswordAuthenticationToken 方针,在这个结构器内部会将对应的信息赋值给各自的本地变量,而且会调用父类 AbstractAuthenticationToken 结构器,传一个 null值进去,为什么是 null 呢?由于刚开端并没有认证,因而用户没有任何权限,而且设置没有认证的信息(setAuthenticated(false)),最终会进入AuthenticationManager 接口的完结类 ProviderManager 中,接着就调用 authenticate 办法。

看到这儿是不是有点熟悉,这不就来到了前一篇文章中说到的 DaoAuthenticationProvider 嘛,它的父类是 AbstractUserDetailsAuthenticationProvider,其间就包含 authenticate 办法,这儿就不重复介绍了。

综上可知,UsernamePasswordAuthenticationFilterDaoAuthenticationProvider 是 SpringSecurity 认证处理的中心逻辑,首要是校验输入的账号密码是否合规。需求留意的是,之前的事例中,咱们都没有 /login 的处理逻辑,全权交由 SpringScurity 处理,在实践运用中咱们并不会这样做。

现在回看一下咱们的需求,即登录认证成功后,之后拜访其他接口需求带着认证成果,能够是 token,后端需求验证认证成果是否有用。那么就要求咱们自定义一个过滤器,针对每次恳求带着的认证成果。那么首要需求自定义认证逻辑,即处理用户调用 /login 的逻辑,最终回来认证成果,所以咱们要剔除掉 UsernamePasswordAuthenticationFilter。这儿咱们暂时不介绍认证逻辑,要点关注如何解析认证成果。

OncePerRequestFilter

OncePerRequestFilter 是 Spring Boot 里边的一个过滤器笼统类,其同样在 Spring Security 里边被广泛用到,这个过滤器笼统类通常被用于承继完结并在每次恳求时只履行一次过滤。

OncePerRequestFilter 承继 Filter,而 doFilter 是 Filter 接口中的办法,doFilterInternal是OncePerRequestFilter 中的一个笼统办法

public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
  if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
    HttpServletRequest httpRequest = (HttpServletRequest)request;
    HttpServletResponse httpResponse = (HttpServletResponse)response;
    String alreadyFilteredAttributeName = this.getAlreadyFilteredAttributeName();
    boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
    if (!this.skipDispatch(httpRequest) && !this.shouldNotFilter(httpRequest)) {
      if (hasAlreadyFilteredAttribute) {
        if (DispatcherType.ERROR.equals(request.getDispatcherType())) {
          this.doFilterNestedErrorDispatch(httpRequest, httpResponse, filterChain);
          return;
        }
        filterChain.doFilter(request, response);
      } else {
        request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
        try {
          this.doFilterInternal(httpRequest, httpResponse, filterChain);
        } finally {
          request.removeAttribute(alreadyFilteredAttributeName);
        }
      }
    } else {
      filterChain.doFilter(request, response);
    }
  } else {
    throw new ServletException("OncePerRequestFilter just supports HTTP requests");
  }
}

由上可知,OncePerRequestFilter.doFilter 办法中经过 request.getAttribute 判别当时过滤器是否已履行,若未履行过,则调用doFilterInternal办法,交由其子类完结。经测验发现,SpringSecurity 的过滤器都会履行 doFilterInternal 办法。

所以咱们自定义过滤器,只需求承继 OncePerRequestFilter,并重写 doFilterInternal 办法。

@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
  @Autowired
  private MyUserDetailsService userDetailsService;
  @Autowired
  private JwtTokenUtil jwtTokenUtil;
  @Value("${jwt.tokenHeader}")
  private String tokenHeader;
  @Value("${jwt.tokenHead}")
  private String tokenHead;
  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
    String authHeader = request.getHeader(this.tokenHeader);
    if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
      String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
      String username = jwtTokenUtil.getUserNameFromToken(authToken);
      logger.info("checking username:" + username);
      if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
        UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
        if (jwtTokenUtil.validateToken(authToken, userDetails)) {
          UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
              userDetails, null, userDetails.getAuthorities());
          authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
          logger.info("authenticated user:" + username);
          SecurityContextHolder.getContext().setAuthentication(authentication);
        }
      }
    }
    filterChain.doFilter(request, response);
  }
}

上述办法大致流程如下:

  1. 从恳求头获取token信息;
  2. 假如 token 不为 null,且格局正确,则获取 token 中要害信息,然后调用 JWT 工具类依据 token 解析出用户名。依据用户名去数据库里获取详细信息,与 token 进行校验,匹配成功则将用户信息封装到 UsernamePasswordAuthenticationToken 方针中,并设置到安全上下文中。

在 SecurityConfig 中这样装备自定义的过滤器:

http.addFilterBefore(jwtAuthenticationTokenFilter,
            UsernamePasswordAuthenticationFilter.class);// 自定义认证过滤器

自定义权限处理

Spring Security 能够经过 http.authorizeRequests() 对web恳求进行授权维护。Spring Security 运用标准Filter建立了对web恳求的阻拦,最终完结对资源的授权拜访。授权流程如下:

Spring Security结合JWT实现认证与授权

剖析授权流程:

  1. 阻拦恳求,已认证用户拜访受维护的web资源将被 SecurityFilterChain 中的 FilterSecurityInterceptor 的子类阻拦。

  2. 获取资源拜访战略,FilterSecurityInterceptor 会从 SecurityMetadataSource 的子类 DefaultFilterInvocationSecurityMetadataSource 获取要拜访当时资源所需求的权限 Collection 。

SecurityMetadataSource 其实便是读取拜访战略的笼统,而读取的内容,其实便是咱们装备的拜访规矩, 读取拜访战略如:

http.csrf().disable()   //屏蔽CSRF操控,即springsecurity不再约束CSRF
.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")

详细来说是 DefaultFilterInvocationSecurityMetadataSource 文件中的 getAttributes()办法,该办法会读取上述拜访规矩,然后封装到 Collection 方针中。假如咱们自定义 SecurityMetadataSource 完结类,则不会再履行 DefaultFilterInvocationSecurityMetadataSource 代码逻辑,即便装备 antMatchers("/r/r1").hasAuthority("p1") 也是无用。

  1. 最终,FilterSecurityInterceptor 会调用 AccessDecisionManager 进行授权决议计划,若决议计划经过,则答应拜访资 源,否则将禁止拜访。

AccessDecisionManager(拜访决议计划办理器)的中心接口如下:

publicinterfaceAccessDecisionManager{
 /** 
 *经过传递的参数来决定用户是否有拜访对应受维护资源的权限 
 */ 
voiddecide(Authenticationauthentication,Objectobject,Collection<ConfigAttribute>
configAttributes)throwsAccessDeniedException,InsufficientAuthenticationException;
//略.. 
}

这儿着重阐明一下decide的参数:

  • authentication:要拜访资源的拜访者的身份
  • object:要拜访的受维护资源,web恳求对应FilterInvocation
  • configAttributes:是受维护资源的拜访战略,经过SecurityMetadataSource获取。

decide接口便是用来鉴定当时用户是否有拜访对应受维护资源的权限。

AccessDecisionManager

自定义 AccessDecisionManager: 完结授权逻辑校验,decide 办法恳求参数中的 configAttributes 能够经过咱们自定义的 SecurityMetadataSource 完结类获取。

public class DynamicAccessDecisionManager implements AccessDecisionManager {
  @Override
  public void decide(Authentication authentication, Object object,
      Collection<ConfigAttribute> configAttributes)
      throws AccessDeniedException, InsufficientAuthenticationException {
    // 当接口未被装备资源时直接放行
    if (CollUtil.isEmpty(configAttributes)) {
      return;
    }
    Iterator<ConfigAttribute> iterator = configAttributes.iterator();
    while (iterator.hasNext()) {
      ConfigAttribute configAttribute = iterator.next();
      //将拜访所需资源或用户具有资源进行比对
      String needAuthority = configAttribute.getAttribute();
      for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
        if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
          return;
        }
      }
    }
    throw new AccessDeniedException("抱愧,您没有拜访权限");
  }
  @Override
  public boolean supports(ConfigAttribute attribute) {
    return true;
  }
  @Override
  public boolean supports(Class<?> clazz) {
    return true;
  }
}

SecurityMetadataSource

DynamicSecurityMetadataSource 与 DefaultFilterInvocationSecurityMetadataSource 同等级,不会读取 SecurityConfig 文件中装备的 antMatchers("/r/r1").hasAuthority("p1")

public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
  private static Map<String, ConfigAttribute> configAttributeMap = null;
  @Autowired
  private DynamicSecurityService dynamicSecurityService;
  @PostConstruct
  public void loadDataSource() {
    configAttributeMap = dynamicSecurityService.loadDataSource();
  }
  public void clearDataSource() {
    configAttributeMap.clear();
    configAttributeMap = null;
  }
  @Override
  public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
    if (configAttributeMap == null) {
      this.loadDataSource();
    }
    List<ConfigAttribute> configAttributes = new ArrayList<>();
    //获取当时拜访的路径
    String url = ((FilterInvocation) object).getRequestUrl();
    String path = URLUtil.getPath(url);
    PathMatcher pathMatcher = new AntPathMatcher();
    Iterator<String> iterator = configAttributeMap.keySet().iterator();
    //获取拜访该路径所需资源
    while (iterator.hasNext()) {
      String pattern = iterator.next();
      if (pathMatcher.match(pattern, path)) {
        configAttributes.add(configAttributeMap.get(pattern));
      }
    }
    // 未设置操作恳求权限,回来空集合
    return configAttributes;
  }
  @Override
  public Collection<ConfigAttribute> getAllConfigAttributes() {
    return null;
  }
  @Override
  public boolean supports(Class<?> clazz) {
    return true;
  }
}

DynamicSecurityService 用来读取 permission 表,获取权限装备。

@Service
public class DynamicSecurityService {
  @Autowired
  private PermissionMapper permissionMapper;
  // 加载资源ANT通配符和资源对应MAP
  public Map<String, ConfigAttribute> loadDataSource() {
    Map<String, ConfigAttribute> urlAndResourceNameMap = new ConcurrentHashMap<>();
    List<Permission> permissions = permissionMapper.findAll();
    permissions.forEach(permission -> urlAndResourceNameMap
        .put(permission.getUrl(), new SecurityConfig(permission.getName())));
    return urlAndResourceNameMap;
  }
}

FilterSecurityInterceptor

FilterSecurityInterceptor 阻拦器,用于判别当时恳求身份认证是否成功,是否有相应的权限,当身份认证失利或许权限缺乏的时分便会抛出相应的反常;

Spring Security运用FilterSecurityInterceptor过滤器来进行URL权限校验,实践运用流程大致如下:

  1. 经过数据库动态装备url资源权限
  2. 体系启动时,经过FilterSecurityInterceptor滤器到数据库加载体系资源权限列表
  3. 用户登陆时经过自定义的UserDetailsService加载当时用户的人物列表
  4. 当有恳求拜访时,经过FilterSecurityInterceptor对比体系资源权限列表和用户资源权限列表(在用户登录时添加到用户信息中)来判别用户是否有该url的拜访权限。

自定义URL权限验证需求在FilterSecurityInterceptor自定义的装备项

  1. DynamicSecurityMetadataSource:完结FilterInvocationSecurityMetadataSource接口,在完结类中加载资源权限,并在filterSecurityInterceptor中注入该完结类。
  2. DynamicAccessDecisionManager:经过完结AccessDecisionManager接口自定义一个决议计划办理器,判别是否有拜访权限。判别逻辑能够写在决议计划办理器的决议计划办法中,也能够经过投票器完结,除了结构供给的三种投票器还能够添加自定义投票器。自定义投票器经过完结AccessDecisionVoter接口来完结。

详细代码如下:

public class DynamicSecurityFilter extends AbstractSecurityInterceptor implements Filter {
  @Autowired
  private IgnoreUrlsConfig ignoreUrlsConfig;
  @Autowired
  private DynamicSecurityMetadataSource dynamicSecurityMetadataSource;
  @Autowired
  public void myAccessDecisionManager(DynamicAccessDecisionManager dynamicAccessDecisionManager) {
    super.setAccessDecisionManager(dynamicAccessDecisionManager);
  }
  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
      FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) servletRequest;
    FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
    //OPTIONS恳求直接放行
    if (request.getMethod().equals(HttpMethod.OPTIONS.toString())) {
      fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
      return;
    }
    //白名单恳求直接放行
    PathMatcher pathMatcher = new AntPathMatcher();
    for (String path : ignoreUrlsConfig.getUrls()) {
      if (pathMatcher.match(path, request.getRequestURI())) {
        fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        return;
      }
    }
    //此处会调用AccessDecisionManager中的decide办法进行鉴权操作
    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 SecurityMetadataSource obtainSecurityMetadataSource() {
    return dynamicSecurityMetadataSource;
  }
}

在 yaml 文件中设置白名单,然后读取这些 api。

secure:
  ignored:
    urls: #安全路径白名单
      - /swagger-ui/
      - /swagger-resources/**
      - /**/v2/api-docs
      - /login
      - /register

IgnoreUrlsConfig 相当于之前 SecurityConfig 文件中的 http.antMatchers("").permitAll()

@Getter
@Setter
@ConfigurationProperties(prefix = "secure.ignored")
public class IgnoreUrlsConfig {
  private List<String> urls = new ArrayList<>();
}

自定义反常处理

Spring Security 中的反常首要分为两大类:一类是认证反常,另一类是授权相关的反常。

HttpSecurity 供给的 exceptionHandling() 办法用来供给反常处理。该办法结构出 ExceptionHandlingConfigurer 反常处理装备类。该装备类供给了两个实用接口:

  • AuthenticationEntryPoint 该类用来一致处理 AuthenticationException 反常
  • AccessDeniedHandler 该类用来一致处理 AccessDeniedException 反常

AuthenticationEntryPoint

被 ExceptionTranslationFilter 用来作为认证方案的进口,即当用户恳求处理过程中遇见认证反常时,被反常处理器(ExceptionTranslationFilter)用来开启特定的认证流程。接口定义如下:

public interface AuthenticationEntryPoint {
  void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException;
}

其间,request 是遇到了认证反常的用户恳求,response 是即将回来给用户的呼应,authException 恳求过程中遇见的认证反常。

自定义 AuthenticationEntryPoint 完结类如下:

public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException authException) throws IOException, ServletException {
    response.setHeader("Access-Control-Allow-Origin", "*");
    response.setHeader("Cache-Control", "no-cache");
    response.setCharacterEncoding("UTF-8");
    response.setContentType("application/json");
    response.getWriter().println(JSONUtil.parse(Result.unauthorized(authException.getMessage())));
    response.getWriter().flush();
  }
}

Spring Security Web 内置AuthenticationEntryPoint完结类

Spring Security WebAuthenticationEntryPoint供给了一些内置完结 :

Http403ForbiddenEntryPoint

设置呼应状态字为403,并非触发一个真实的认证流程。通常在一个预验证(pre-authenticated authentication)现已得出结论需求回绝用户恳求的情况被用于回绝用户恳求。

HttpStatusEntryPoint

设置特定的呼应状态字,并非触发一个真实的认证流程。

LoginUrlAuthenticationEntryPoint

依据装备计算出登录页面url,将用户重定向到该登录页面从而开端一个认证流程。

BasicAuthenticationEntryPoint

对应标准Http Basic认证流程的触发动作,向呼应写入状态字401和头部WWW-Authenticate:"Basic realm="xxx"触发标准Http Basic认证流程。

DigestAuthenticationEntryPoint

对应标准Http Digest认证流程的触发动作,向呼应写入状态字401和头部WWW-Authenticate:"Digest realm="xxx"触发标准Http Digest认证流程。

DelegatingAuthenticationEntryPoint

这是一个署理,将认证任务委托给所署理的多个AuthenticationEntryPoint方针,其间一个被标记为缺省AuthenticationEntryPoint。

AccessDeniedHandler

处理授权反常,接口定义如下:

public interface AccessDeniedHandler {
  void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException;
}

自定义 AccessDeniedHandler 完结类如下:

public class MyAccessDeniedHandler implements AccessDeniedHandler {
  @Override
  public void handle(HttpServletRequest request, HttpServletResponse response,
      AccessDeniedException accessDeniedException) throws IOException, ServletException {
    response.setHeader("Access-Control-Allow-Origin", "*");
    response.setHeader("Cache-Control", "no-cache");
    response.setCharacterEncoding("UTF-8");
    response.setContentType("application/json");
    response.getWriter()
        .println(JSONUtil.parse(Result.forbidden(accessDeniedException.getMessage())));
    response.getWriter().flush();
  }
}

关于上述自定义的认证与授权处理,以及反常处理,需求在 SecurityConfig 文件中加以装备,如下所示:

@Configuration
public class SecurityConfig {
  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }
  @Bean
  public IgnoreUrlsConfig ignoreUrlsConfig() {
    return new IgnoreUrlsConfig();
  }
  @Bean
  public MyAccessDeniedHandler myAccessDeniedHandler() {
    return new MyAccessDeniedHandler();
  }
  @Bean
  public MyAuthenticationEntryPoint myAuthenticationEntryPoint() {
    return new MyAuthenticationEntryPoint();
  }
  @Bean
  public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
    return new JwtAuthenticationTokenFilter();
  }
  @ConditionalOnBean(name = "dynamicSecurityService")
  @Bean
  public DynamicAccessDecisionManager dynamicAccessDecisionManager() {
    return new DynamicAccessDecisionManager();
  }
  @ConditionalOnBean(name = "dynamicSecurityService")
  @Bean
  public DynamicSecurityMetadataSource dynamicSecurityMetadataSource() {
    return new DynamicSecurityMetadataSource();
  }
  @ConditionalOnBean(name = "dynamicSecurityService")
  @Bean
  public DynamicSecurityFilter dynamicSecurityFilter() {
    return new DynamicSecurityFilter();
  }
  //跨域
  @Autowired
  private CorsFilter corsFilter;
  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
        .authorizeRequests();
    //不需求维护的资源路径答应拜访
    for (String url : ignoreUrlsConfig().getUrls()) {
      registry.antMatchers(url).permitAll();
    }
    //答应跨域恳求的OPTIONS恳求
    registry.antMatchers(HttpMethod.OPTIONS)
        .permitAll();
    registry.and()
        .csrf().disable()   //屏蔽CSRF操控,即spring security不再约束CSRF
        .authorizeRequests()
        .anyRequest().authenticated();
    registry.and()
        .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .addFilterBefore(corsFilter, CsrfFilter.class)//跨域装备
        .exceptionHandling()  //反常处理,下面是自定义的两个反常
        .accessDeniedHandler(myAccessDeniedHandler())//授权反常捕获
        .authenticationEntryPoint(myAuthenticationEntryPoint())//认证反常捕获
        .and()
        .addFilterBefore(jwtAuthenticationTokenFilter(),
            UsernamePasswordAuthenticationFilter.class);// 自定义认证过滤器
    //添加动态权限校验过滤器
    registry.and().addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class);
    return http.build();
  }
}

JWT

那么如何运用 JWT 呢?此时咱们需求运用一个叫做 JJWT 的库。

JJWT

JJWT 是一个供给端到端的 JWT 创建和验证的 Java 库。永久免费和开源(Apache License,版本2.0),JJWT 很简略运用和了解。它被设计成一个以修建为中心的流畅界面,躲藏了它的大部分复杂性。

  • JJWT 的方针是最简略运用和了解用于在 JVM 上创建和验证 JSON Web 令牌(JWTs)的库。
  • JJWT 是依据 JWT、JWS、JWE、JWK 和 JWA RFC标准的Java完结。
  • JJWT 还添加了一些不归于标准的便当扩展,比方 JWT 紧缩和索赔强制。

JJWT 标准兼容

  • 创建和解析明文紧缩JWTs
  • 创建、解析和验证所有标准JWS算法的数字签名紧缩JWTs(又称JWSs):
  • HS256:运用SHA-256的HMAC
  • HS384:运用SHA-384的HMAC
  • HS512:运用SHA-512的HMAC
  • RS256:运用SHA-256的RSASSA-PKCS-v1_5
  • RS384:运用SHA-384的RSASSA-PKCS-v1_5
  • RS512:运用SHA-512的RSASSA-PKCS-v1_5
  • PS256:运用SHA-256的RSASSA-PSS和运用SHA-256的MGF1
  • PS384:运用SHA-384的RSASSA-PSS和运用SHA-384的MGF1
  • PS512:运用SHA-512的RSASSA-PSS和运用SHA-512的MGF1
  • ES256:运用P-256和SHA-256的ECDSA
  • ES384:运用P-384和SHA-384的ECDSA
  • ES512:运用P-521和SHA-512的ECDSA

实践运用

导入 maven 依赖

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

JWT token的工具类

用于生成和解析JWT token的工具类

/**
 * JwtToken生成的工具类 JWT token的格局:header.payload.signature header的格局(算法、token的类型): {"alg":
 * "HS512","typ": "JWT"} payload的格局(用户名、创建时刻、生成时刻): {"sub":"wang","created":1489079981393,"exp":1489684781}
 * signature的生成算法: HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
 */
@Slf4j
@Component
public class JwtTokenUtil {
  private static final String CLAIM_KEY_USERNAME = "sub";
  private static final String CLAIM_KEY_CREATED = "created";
  @Value("${jwt.secret}")
  private String secret;
  @Value("${jwt.expiration}")
  private Long expiration;
  @Value("${jwt.tokenHead}")
  private String tokenHead;
  /**
   * 依据担任生成JWT的token
   */
  private String generateToken(Map<String, Object> claims) {
    return Jwts.builder()
        .setClaims(claims)
        .setExpiration(generateExpirationDate())
        .signWith(SignatureAlgorithm.HS512, secret)
        .compact();
  }
  /**
   * 从token中获取JWT中的负载
   */
  private Claims getClaimsFromToken(String token) {
    Claims claims = null;
    try {
      claims = Jwts.parser()
          .setSigningKey(secret)
          .parseClaimsJws(token)
          .getBody();
    } catch (Exception e) {
      log.info("JWT格局验证失利:{}", token);
    }
    return claims;
  }
  /**
   * 生成token的过期时刻
   */
  private Date generateExpirationDate() {
    return new Date(System.currentTimeMillis() + expiration * 1000);
  }
  /**
   * 从token中获取登录用户名
   */
  public String getUserNameFromToken(String token) {
    String username;
    try {
      Claims claims = getClaimsFromToken(token);
      username = claims.getSubject();
    } catch (Exception e) {
      username = null;
    }
    return username;
  }
  /**
   * 验证token是否还有用
   *
   * @param token       客户端传入的token
   * @param userDetails 从数据库中查询出来的用户信息
   */
  public boolean validateToken(String token, UserDetails userDetails) {
    String username = getUserNameFromToken(token);
    return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
  }
  /**
   * 判别token是否现已失效
   */
  private boolean isTokenExpired(String token) {
    Date expiredDate = getExpiredDateFromToken(token);
    return expiredDate.before(new Date());
  }
  /**
   * 从token中获取过期时刻
   */
  private Date getExpiredDateFromToken(String token) {
    Claims claims = getClaimsFromToken(token);
    return claims.getExpiration();
  }
  /**
   * 依据用户信息生成token
   */
  public String generateToken(UserDetails userDetails) {
    Map<String, Object> claims = new HashMap<>();
    claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
    claims.put(CLAIM_KEY_CREATED, DateUtil.date());
    return generateToken(claims);
  }
  /**
   * 当本来的token没过期时是能够改写的
   *
   * @param oldToken 带tokenHead的token
   */
  public String refreshHeadToken(String oldToken) {
    if (StrUtil.isEmpty(oldToken)) {
      return null;
    }
    String token = oldToken.substring(tokenHead.length());
    if (StrUtil.isEmpty(token)) {
      return null;
    }
    //token校验不经过
    Claims claims = getClaimsFromToken(token);
    if (Objects.isNull(claims)) {
      return null;
    }
    //假如token现已过期,不支持改写
    if (isTokenExpired(token)) {
      return null;
    }
    //假如token在30分钟之内刚改写过,回来原token
    if (tokenRefreshJustBefore(token, 30 * 60)) {
      return token;
    } else {
      claims.put(CLAIM_KEY_CREATED, new Date());
      return generateToken(claims);
    }
  }
  /**
   * 判别token在指定时刻内是否刚刚改写过
   *
   * @param token 原token
   * @param time  指定时刻(秒)
   */
  private boolean tokenRefreshJustBefore(String token, int time) {
    Claims claims = getClaimsFromToken(token);
    Date created = claims.get(CLAIM_KEY_CREATED, Date.class);
    Date refreshDate = new Date();
    //改写时刻在创建时刻的指定时刻内
    if (refreshDate.after(created) && refreshDate.before(DateUtil.offsetSecond(created, time))) {
      return true;
    }
    return false;
  }
}

项目实践

数据库

稍微复杂点的后台体系都会涉及到用户权限办理,已然咱们选择运用 Spring Security 这一安全结构,那么就需求考虑如何来设计一套权限办理体系。首要需求知道的是,权限便是对数据(体系的实体类)和数据可进行的操作(增删查改)的会集办理。要构建一个可用的权限办理体系,涉及到三个中心类:一个是用户User,一个是人物Role,最终是权限Permission

用户人物,人物权限都是多对多关系,即一个用户具有多个人物,一个人物归于多个用户;一个人物具有多个权限,一个权限归于多个人物。这种办法需求指定用户有哪些人物,而人物又有哪些权限。

履行如下 SQL 语句,来构建数据表并初始化数据。

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) DEFAULT NULL,
  `password` varchar(100) DEFAULT NULL,
  `phone` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';
CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL,
  `desc` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='人物表';
INSERT into `role`(name,`desc`) values('admin','办理员');
INSERT into `role`(name,`desc`) values('role1','人物1');
INSERT into `role`(name,`desc`) values('role2','人物2');
CREATE TABLE `permission` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL,
  `url` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='权限表';
INSERT into permission(name,url) values('all','/*');
INSERT into permission(name,url) values('home','/home/*');
INSERT into permission(name,url) values('product','/product/*');
INSERT into permission(name,url) values('customer','/customer/*');
CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `uid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `users_role_ibfk_1` (`uid`),
  KEY `users_role_ibfk_2` (`rid`),
  CONSTRAINT `users_role_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `user` (`id`),
  CONSTRAINT `users_role_ibfk_2` FOREIGN KEY (`rid`) REFERENCES `role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户人物对照表';
CREATE TABLE `role_permission` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `rid` int(11) DEFAULT NULL ,
  `pid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `role_permission_ibfk_1` (`rid`),
  KEY `role_permission_ibfk_2` (`pid`),
  CONSTRAINT `role_permission_ibfk_1` FOREIGN KEY (`rid`) REFERENCES `role` (`id`),
  CONSTRAINT `role_permission_ibfk_2` FOREIGN KEY (`pid`) REFERENCES `permission` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='人物权限对照表';

待会咱们能够注册用户,对照表数据则需求在数据库中手动新增,暂时未供给相关接口。

代码

项目中有两个 controller 文件,一个用于用户登录,另一个资源拜访,这儿简略贴一下代码,感兴趣的能够去我的 github 上下载源码。

@RestController
public class UserController {
  @Autowired
  private UserService userService;
  @Value("${jwt.tokenHeader}")
  private String tokenHeader;
  @Value("${jwt.tokenHead}")
  private String tokenHead;
  @PostMapping("/register")
  public Result register(@RequestBody UserRequest userRequest) {
    userService.register(userRequest);
    return Result.ok();
  }
  @PostMapping("/login")
  public Result<Object> login(@RequestParam("username") String username,
      @RequestParam("password") String password) {
    String token = userService.login(username, password);
    Map<String, String> tokenMap = new HashMap<>();
    tokenMap.put("token", token);
    tokenMap.put("tokenHead", tokenHead);
    return Result.ok(tokenMap);
  }
  @PostMapping("/refreshToken")
  public Result<Object> refreshToken(@RequestBody HttpServletRequest request) {
    String token = request.getHeader(tokenHeader);
    String refreshToken = userService.refreshToken(token);
    Map<String, String> tokenMap = new HashMap<>();
    tokenMap.put("token", refreshToken);
    tokenMap.put("tokenHead", tokenHead);
    return Result.ok(tokenMap);
  }
}
@RestController
public class ResourceController {
  @GetMapping("/home/level1")
  public Result getHomeLevel1() {
    return Result.ok("获取拜访Home目录下的Level1的权限");
  }
  @GetMapping("/home/level2")
  public Result getHomeLevel2() {
    return Result.ok("获取拜访Home目录下的Level2的权限");
  }
  @GetMapping("/customer/level1")
  public Result getCustomerLevel1() {
    return Result.ok("获取拜访Customer目录下的Level1的权限");
  }
  @GetMapping("/customer/level2")
  public Result getCustomerLevel2() {
    return Result.ok("获取拜访Customer目录下的Level2的权限");
  }
  @GetMapping("/product/level1")
  public Result getProductLevel1() {
    return Result.ok("获取拜访Product目录下的Level3的权限");
  }
  @GetMapping("/product/level2")
  public Result getProductLevel2() {
    return Result.ok("获取拜访Product目录下的Level的权限");
  }
}

首要咱们需求自定义 UserDetails

@Setter
@Builder
public class MyUserDetails implements UserDetails {
  private User user;
  private List<Permission> permissionList;
  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return permissionList.stream()
        .map(permission -> new SimpleGrantedAuthority(permission.getName())).collect(
            Collectors.toList());
  }
  @Override
  public String getPassword() {
    return user.getPassword();
  }
  @Override
  public String getUsername() {
    return user.getUsername();
  }
  @Override
  public boolean isAccountNonExpired() {
    return true;
  }
  @Override
  public boolean isAccountNonLocked() {
    return true;
  }
  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }
  @Override
  public boolean isEnabled() {
    return !Objects.isNull(user);
  }
}

接着处理自定义 UserDetailsService

@Component
@RequiredArgsConstructor
public class MyUserDetailsService implements UserDetailsService {
  private final UserMapper userMapper;
  private final PermissionMapper permissionMapper;
  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    //依据账号去数据库查询...
    User user = userMapper.selectByUserName(username);
    if (!Objects.isNull(user)) {
      List<Permission> permissionList = permissionMapper.findPermissionsByUserId(user.getId());
      return MyUserDetails.builder().user(user).permissionList(permissionList).build();
    }
    throw new UsernameNotFoundException("用户名或密码错误");
  }
}

以及处理用户注册登录的服务

@Service
@Slf4j
@RequiredArgsConstructor
public class UserService {
  private final MyUserDetailsService userDetailsService;
  private final PasswordEncoder passwordEncoder;
  private final JwtTokenUtil jwtTokenUtil;
  private final UserStruct userStruct;
  private final UserMapper userMapper;
  public String login(String username, String password) {
    String token = null;
    try {
      UserDetails userDetails = userDetailsService.loadUserByUsername(username);
      if (!passwordEncoder.matches(password, userDetails.getPassword())) {
        BusinessException.fail("密码不正确");
      }
      if (!userDetails.isEnabled()) {
        BusinessException.fail("帐号已被禁用");
      }
      UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
          userDetails, null, userDetails.getAuthorities());
      SecurityContextHolder.getContext().setAuthentication(authentication);
      token = jwtTokenUtil.generateToken(userDetails);
    } catch (AuthenticationException e) {
      log.error("登录反常,detail" + e.getMessage());
    }
    return token;
  }
  public void register(UserRequest userRequest) {
    User user = userMapper.selectByUserName(userRequest.getUsername());
    if (Objects.nonNull(user)) {
      BusinessException.fail("用户名已存在!");
    }
    String encodePassword = passwordEncoder.encode(userRequest.getPassword());
    User obj = userStruct.toUser(userRequest);
    obj.setPassword(encodePassword);
    userMapper.insert(obj);
  }
  public String refreshToken(String oldToken) {
    return jwtTokenUtil.refreshHeadToken(oldToken);
  }
}

测验

注册用户

Spring Security结合JWT实现认证与授权

用户登录并回来 token

Spring Security结合JWT实现认证与授权

手动给 hresh3 用户赋予 role1 人物,即具备 home 目录下的拜访权限。

仿制登录后获取到的 token,拜访 home/level1,能够正常拜访。

Spring Security结合JWT实现认证与授权

假如想要拜访 customer 目录,则会提示无权拜访。

Spring Security结合JWT实现认证与授权

总结

《从零打造项目》系列的文章并不是一口气就写完了的,有些知识点也是边学边用,比方 SpringSecurity 安全结构,尽管之前简略学过,但仅限于皮毛,底层逻辑不了解,更无法独自造轮子。关于 SpringSecurity 的学习其实远不止这些内容,想要继续学习推荐我们阅览《深入浅出 Spring Security》,或许作者在网上发布的一系列文章。

本文贴合实践运用,详细介绍了如何自定义认证和授权逻辑,测验代码根本满足一个简略项目的需求。至此,关于 SpringSecurity 的学习暂时到此为止,现在把握的内容差不多能够满足项目需求,所以接下来我会继续完结商城项目的开发。

参考文献

SpringSecurity系列 之 AuthenticationEntryPoint接口及其完结类的用法

Spring Security 实战干货:自定义反常处理

spring security中自定义AccessDeniedHandler不生效的实验记录

OncePerRequestFilter的效果

Spring Security教程(八):用户认证流程源码详解

Spring Security 认证流程源码详解

spring boot security 授权–自定义AccessDecisionManager和AccessDecisionVoter