SpringBoot集成SpringSecurity和JWT做登陆鉴权的实现

废话

目前流行的前后端分离让Java程序员可以更加专注的做好后台业务逻辑的功能实现,提供如返回Json格式的数据接口就可以。SpringBoot的易用性和对其他框架的高度集成,用来快速开发一个小型应用是最佳的选择。

一套前后端分离的后台项目,刚开始就要面对的就是登陆和授权的问题。这里提供一套方案供大家参考。

主要看点:

  • 登陆后获取token,根据token来请求资源
  • 根据用户角色来确定对资源的访问权限
  • 统一异常处理
  • 返回标准的Json格式数据

正文

首先是pom文件:

<dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <!--这是不是必须,只是我引用了里面一些类的方法-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-solr</artifactId>
    </dependency>
        <!--这是不是必须,只是我引用了里面一些类的方法-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>1.3.2</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger2</artifactId>
      <version>2.6.1</version>
    </dependency>
    <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger-ui</artifactId>
      <version>2.6.1</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-rest</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-jwt</artifactId>
      <version>1.0.9.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.9.0</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

application.yml:

spring :
 datasource :
  url : jdbc:mysql://127.0.0.1:3306/les_data_center?useUnicode=true&amp;characterEncoding=UTF-8&allowMultiQueries=true&useAffectedRows=true&useSSL=false
  username : root
  password : 123456
  driverClassName : com.mysql.jdbc.Driver
 jackson:
  data-format: yyyy-MM-dd HH:mm:ss
  time-zone: GMT+8
mybatis :
 config-location : classpath:/mybatis-config.xml
# JWT
jwt:
 header: Authorization
 secret: mySecret
 #token有效期一天
 expiration: 86400
 tokenHead: "Bearer "

接着是对security的配置,让security来保护我们的API

SpringBoot推荐使用配置类来代替xml配置。那这里,我也使用配置类的方式。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  private final JwtAuthenticationEntryPoint unauthorizedHandler;

  private final AccessDeniedHandler accessDeniedHandler;

  private final UserDetailsService CustomUserDetailsService;

  private final JwtAuthenticationTokenFilter authenticationTokenFilter;

  @Autowired
  public WebSecurityConfig(JwtAuthenticationEntryPoint unauthorizedHandler,
               @Qualifier("RestAuthenticationAccessDeniedHandler") AccessDeniedHandler accessDeniedHandler,
               @Qualifier("CustomUserDetailsService") UserDetailsService CustomUserDetailsService,
               JwtAuthenticationTokenFilter authenticationTokenFilter) {
    this.unauthorizedHandler = unauthorizedHandler;
    this.accessDeniedHandler = accessDeniedHandler;
    this.CustomUserDetailsService = CustomUserDetailsService;
    this.authenticationTokenFilter = authenticationTokenFilter;
  }

  @Autowired
  public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
    authenticationManagerBuilder
        // 设置UserDetailsService
        .userDetailsService(this.CustomUserDetailsService)
        // 使用BCrypt进行密码的hash
        .passwordEncoder(passwordEncoder());
  }
  // 装载BCrypt密码编码器
  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  @Override
  protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
        .exceptionHandling().accessDeniedHandler(accessDeniedHandler).and()
        // 由于使用的是JWT,我们这里不需要csrf
        .csrf().disable()
        .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
        // 基于token,所以不需要session
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

        .authorizeRequests()

        // 对于获取token的rest api要允许匿名访问
        .antMatchers("/api/v1/auth", "/api/v1/signout", "/error/**", "/api/**").permitAll()
        // 除上面外的所有请求全部需要鉴权认证
        .anyRequest().authenticated();

    // 禁用缓存
    httpSecurity.headers().cacheControl();

    // 添加JWT filter
    httpSecurity
        .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
  }

  @Override
  public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/v2/api-docs",
        "/swagger-resources/configuration/ui",
        "/swagger-resources",
        "/swagger-resources/configuration/security",
        "/swagger-ui.html"
    );
  }

  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  }
}

该类中配置了几个bean来供security使用。

  1. JwtAuthenticationTokenFilter:token过滤器来验证token有效性
  2. UserDetailsService:实现了DetailsService接口,用来做登陆验证
  3. JwtAuthenticationEntryPoint :认证失败处理类
  4. RestAuthenticationAccessDeniedHandler: 权限不足处理类

那么,接下来一个一个实现这些类:

/**
 * token校验,引用的stackoverflow一个答案里的处理方式
 * Author: JoeTao
 * createAt: 2018/9/14
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

  @Value("${jwt.header}")
  private String token_header;

  @Resource
  private JWTUtils jwtUtils;

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
    String auth_token = request.getHeader(this.token_header);
    final String auth_token_start = "Bearer ";
    if (StringUtils.isNotEmpty(auth_token) && auth_token.startsWith(auth_token_start)) {
      auth_token = auth_token.substring(auth_token_start.length());
    } else {
      // 不按规范,不允许通过验证
      auth_token = null;
    }

    String username = jwtUtils.getUsernameFromToken(auth_token);

    logger.info(String.format("Checking authentication for user %s.", username));

    if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
      User user = jwtUtils.getUserFromToken(auth_token);
      if (jwtUtils.validateToken(auth_token, user)) {
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        logger.info(String.format("Authenticated user %s, setting security context", username));
        SecurityContextHolder.getContext().setAuthentication(authentication);
      }
    }
    chain.doFilter(request, response);
  }
}
/**
 * 认证失败处理类,返回401
 * Author: JoeTao
 * createAt: 2018/9/20
 */
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {

  private static final long serialVersionUID = -8970718410437077606L;

  @Override
  public void commence(HttpServletRequest request,
             HttpServletResponse response,
             AuthenticationException authException) throws IOException {
    //验证为未登陆状态会进入此方法,认证错误
    System.out.println("认证失败:" + authException.getMessage());
    response.setStatus(200);
    response.setCharacterEncoding("UTF-8");
    response.setContentType("application/json; charset=utf-8");
    PrintWriter printWriter = response.getWriter();
    String body = ResultJson.failure(ResultCode.UNAUTHORIZED, authException.getMessage()).toString();
    printWriter.write(body);
    printWriter.flush();
  }
}

因为我们使用的REST API,所以我们认为到达后台的请求都是正常的,所以返回的HTTP状态码都是200,用接口返回的code来确定请求是否正常。

/**
* 权限不足处理类,返回403
 * Author: JoeTao
 * createAt: 2018/9/21
 */
@Component("RestAuthenticationAccessDeniedHandler")
public class RestAuthenticationAccessDeniedHandler implements AccessDeniedHandler {
  @Override
  public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
    //登陆状态下,权限不足执行该方法
    System.out.println("权限不足:" + e.getMessage());
    response.setStatus(200);
    response.setCharacterEncoding("UTF-8");
    response.setContentType("application/json; charset=utf-8");
    PrintWriter printWriter = response.getWriter();
    String body = ResultJson.failure(ResultCode.FORBIDDEN, e.getMessage()).toString();
    printWriter.write(body);
    printWriter.flush();
  }
}
/**
 * 登陆身份认证
 * Author: JoeTao
 * createAt: 2018/9/14
 */
@Component(value="CustomUserDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
  private final AuthMapper authMapper;

  public CustomUserDetailsService(AuthMapper authMapper) {
    this.authMapper = authMapper;
  }

  @Override
  public User loadUserByUsername(String name) throws UsernameNotFoundException {
    User user = authMapper.findByUsername(name);
    if (user == null) {
      throw new UsernameNotFoundException(String.format("No user found with username '%s'.", name));
    }
    Role role = authMapper.findRoleByUserId(user.getId());
    user.setRole(role);
    return user;
  }
}

登陆逻辑:

 public ResponseUserToken login(String username, String password) {
    //用户验证
    final Authentication authentication = authenticate(username, password);
    //存储认证信息
    SecurityContextHolder.getContext().setAuthentication(authentication);
    //生成token
    final User user = (User) authentication.getPrincipal();
//    User user = (User) userDetailsService.loadUserByUsername(username);
    final String token = jwtTokenUtil.generateAccessToken(user);
    //存储token
    jwtTokenUtil.putToken(username, token);
    return new ResponseUserToken(token, user);
  }

private Authentication authenticate(String username, String password) {
    try {
      //该方法会去调用userDetailsService.loadUserByUsername()去验证用户名和密码,如果正确,则存储该用户名密码到“security 的 context中”
      return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
    } catch (DisabledException | BadCredentialsException e) {
      throw new CustomException(ResultJson.failure(ResultCode.LOGIN_ERROR, e.getMessage()));
    }
  }

自定义异常:

@Getter
public class CustomException extends RuntimeException{
  private ResultJson resultJson;

  public CustomException(ResultJson resultJson) {
    this.resultJson = resultJson;
  }
}

统一异常处理:

/**
 * 异常处理类
 * controller层异常无法捕获处理,需要自己处理
 * Created by jt on 2018/8/27.
 */
@RestControllerAdvice
@Slf4j
public class DefaultExceptionHandler {

  /**
   * 处理所有自定义异常
   * @param e
   * @return
   */
  @ExceptionHandler(CustomException.class)
  public ResultJson handleCustomException(CustomException e){
    log.error(e.getResultJson().getMsg().toString());
    return e.getResultJson();
  }
}

所有经controller转发的请求抛出的自定义异常都会被捕获处理,一般情况下就是返回给调用方一个json的报错信息,包含自定义状态码、错误信息及补充描述信息。

值得注意的是,在请求到达controller之前,会被Filter拦截,如果在controller或者之前抛出的异常,自定义的异常处理器是无法处理的,需要自己重新定义一个全局异常处理器或者直接处理。

Filter拦截请求两次的问题

跨域的post的请求会验证两次,get不会。网上的解释是,post请求第一次是预检请求,Request Method: OPTIONS。
解决方法:

在webSecurityConfig里添加

.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()

就可以不拦截options请求了。

这里只给出了最主要的代码,还有controller层的访问权限设置,返回状态码,返回类定义等等。

所有代码已上传GitHub,项目地址

到此这篇关于SpringBoot集成SpringSecurity和JWT做登陆鉴权的实现的文章就介绍到这了,更多相关SpringBoot JWT 登陆鉴权内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • SpringBoot Security整合JWT授权RestAPI的实现

    本教程主要详细讲解SpringBoot Security整合JWT授权RestAPI. 基础环境 技术 版本 Java 1.8+ SpringBoot 2.x.x Security 5.x JWT 0.9.0 创建项目 初始化项目 mvn archetype:generate -DgroupId=com.edurt.sli.slisj -DartifactId=spring-learn-integration-security-jwt -DarchetypeArtifactId=maven-ar

  • SpringBoot与spring security的结合的示例

    权限控制,也是我们再日常开发中经常遇到的场景,需要根据用户的角色决定是否可以看到某个资源.目前,市面上此类框架主要有shiro与我们今天要讲到的spring security.关于权限的控制有复杂的控制,例如几乎每个公司都有单点登录系统,根据用户名来到数据库中拿到对应的权限,在展示该权限下能看到的资源.还有一种就是简单的控制,也就是我们今天所要提到的.将账号,密码,角色配置到代码中,也可以进行简单的控制,缺点不言而喻,扩展性不好,只有固定的账号,但是作为演示还是够用的. 好了废话不多说,上pom

  • SpringBoot+Spring Security+JWT实现RESTful Api权限控制的方法

    摘要:用spring-boot开发RESTful API非常的方便,在生产环境中,对发布的API增加授权保护是非常必要的.现在我们来看如何利用JWT技术为API增加授权保护,保证只有获得授权的用户才能够访问API. 一:开发一个简单的API 在IDEA开发工具中新建一个maven工程,添加对应的依赖如下: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-b

  • SpringBoot+Security 发送短信验证码的实现

    在core模块下properties包中创建SmsCodeProperties 在ValidateCodeProperties中new一个SmsCodeProperties对象,并实现getter.setter方法 在core模块下validate包中创建SmsCodeGenerator实现ValidateCodeGenerator接口 创建SmsCodeSender接口,定义发送短信的抽象方法 实现SmsCodeSender接口 在ValidateCodeBeanConfig中把SmsCode

  • SpringBoot + SpringSecurity 环境搭建的步骤

    一.使用SpringBoot+Maven搭建一个多模块项目(可以参考这篇文章 --> 这里) 二.删除父工程的src文件,删除app.browser.core下的.java文件 依赖关系: demo 依赖 browser browser.app依赖core 三.父工程pom.xml文件 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache

  • 详解SpringBoot+SpringSecurity+jwt整合及初体验

    原来一直使用shiro做安全框架,配置起来相当方便,正好有机会接触下SpringSecurity,学习下这个.顺道结合下jwt,把安全信息管理的问题扔给客户端, 准备 首先用的是SpringBoot,省去写各种xml的时间.然后把依赖加入一下 <!--安全--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-secu

  • SpringBoot结合SpringSecurity实现图形验证码功能

    本文介绍了SpringBoot结合SpringSecurity实现图形验证码功能,分享给大家,具体如下: 生成图形验证码 根据随机数生成图片 将随机数存到Session中 将生成的图片写到接口的响应中 生成图形验证码的过程比较简单,和SpringSecurity也没有什么关系.所以就直接贴出代码了 根据随机数生成图片 /** * 生成图形验证码 * @param request * @return */ private ImageCode generate(ServletWebRequest r

  • SpringBoot2.0 整合 SpringSecurity 框架实现用户权限安全管理方法

    一.Security简介 1.基础概念 Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring的IOC,DI,AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为安全控制编写大量重复代码的工作. 2.核心API解读 1).SecurityContextHolder 最基本的对象,保存着当前会话用户认证,权限,鉴权等核心数据.Secu

  • SpringBoot安全认证Security的实现方法

    一.基本环境搭建 父pom依赖 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> </parent> 1. 添加pom依赖: <dependency> <groupId&

  • SpringBoot + SpringSecurity 短信验证码登录功能实现

    实现原理 在之前的文章中,我们介绍了普通的帐号密码登录的方式: SpringBoot + Spring Security 基本使用及个性化登录配置. 但是现在还有一种常见的方式,就是直接通过手机短信验证码登录,这里就需要自己来做一些额外的工作了. 对SpringSecurity认证流程详解有一定了解的都知道,在帐号密码认证的过程中,涉及到了以下几个类:UsernamePasswordAuthenticationFilter(用于请求参数获取),UsernamePasswordAuthentica

随机推荐