如何實(shí)現(xiàn)SpringBoot集成SpringSecurity和JWT做登陸鑒權(quán)

小編這次要給大家分享的是如何實(shí)現(xiàn)SpringBoot集成SpringSecurity和JWT做登陸鑒權(quán),文章內(nèi)容豐富,感興趣的小伙伴可以來了解一下,希望大家閱讀完這篇文章之后能夠有所收獲。

十載的保靖網(wǎng)站建設(shè)經(jīng)驗(yàn),針對設(shè)計(jì)、前端、開發(fā)、售后、文案、推廣等六對一服務(wù),響應(yīng)快,48小時及時工作處理。全網(wǎng)營銷推廣的優(yōu)勢是能夠根據(jù)用戶設(shè)備顯示端的尺寸不同,自動調(diào)整保靖建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設(shè)計(jì),從而大程度地提升瀏覽體驗(yàn)。成都創(chuàng)新互聯(lián)從事“保靖網(wǎng)站設(shè)計(jì)”,“保靖網(wǎng)站推廣”以來,每個客戶項(xiàng)目都認(rèn)真落實(shí)執(zhí)行。

廢話

目前流行的前后端分離讓Java程序員可以更加專注的做好后臺業(yè)務(wù)邏輯的功能實(shí)現(xiàn),提供如返回Json格式的數(shù)據(jù)接口就可以。SpringBoot的易用性和對其他框架的高度集成,用來快速開發(fā)一個小型應(yīng)用是最佳的選擇。

一套前后端分離的后臺項(xiàng)目,剛開始就要面對的就是登陸和授權(quán)的問題。這里提供一套方案供大家參考。

主要看點(diǎn):

  • 登陸后獲取token,根據(jù)token來請求資源
  • 根據(jù)用戶角色來確定對資源的訪問權(quán)限
  • 統(tǒng)一異常處理
  • 返回標(biāo)準(zhǔn)的Json格式數(shù)據(jù)

正文

首先是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&#63;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來保護(hù)我們的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
        // 設(shè)置UserDetailsService
        .userDetailsService(this.CustomUserDetailsService)
        // 使用BCrypt進(jìn)行密碼的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()
        // 除上面外的所有請求全部需要鑒權(quán)認(rèn)證
        .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過濾器來驗(yàn)證token有效性
  2. UserDetailsService:實(shí)現(xiàn)了DetailsService接口,用來做登陸驗(yàn)證
  3. JwtAuthenticationEntryPoint :認(rèn)證失敗處理類
  4. RestAuthenticationAccessDeniedHandler: 權(quán)限不足處理類

那么,接下來一個一個實(shí)現(xiàn)這些類:

/**
 * token校驗(yàn),引用的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 {
      // 不按規(guī)范,不允許通過驗(yàn)證
      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);
  }
}
/**
 * 認(rèn)證失敗處理類,返回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 {
    //驗(yàn)證為未登陸狀態(tài)會進(jìn)入此方法,認(rèn)證錯誤
    System.out.println("認(rèn)證失?。?quot; + 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();
  }
}

因?yàn)槲覀兪褂玫腞EST API,所以我們認(rèn)為到達(dá)后臺的請求都是正常的,所以返回的HTTP狀態(tài)碼都是200,用接口返回的code來確定請求是否正常。

/**
* 權(quán)限不足處理類,返回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 {
    //登陸狀態(tài)下,權(quán)限不足執(zhí)行該方法
    System.out.println("權(quán)限不足:" + 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();
  }
}
/**
 * 登陸身份認(rèn)證
 * 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) {
    //用戶驗(yàn)證
    final Authentication authentication = authenticate(username, password);
    //存儲認(rèn)證信息
    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 {
      //該方法會去調(diào)用userDetailsService.loadUserByUsername()去驗(yàn)證用戶名和密碼,如果正確,則存儲該用戶名密碼到“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;
  }
}

統(tǒng)一異常處理:

/**
 * 異常處理類
 * 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();
  }
}

所有經(jīng)controller轉(zhuǎn)發(fā)的請求拋出的自定義異常都會被捕獲處理,一般情況下就是返回給調(diào)用方一個json的報錯信息,包含自定義狀態(tài)碼、錯誤信息及補(bǔ)充描述信息。

值得注意的是,在請求到達(dá)controller之前,會被Filter攔截,如果在controller或者之前拋出的異常,自定義的異常處理器是無法處理的,需要自己重新定義一個全局異常處理器或者直接處理。

Filter攔截請求兩次的問題

跨域的post的請求會驗(yàn)證兩次,get不會。網(wǎng)上的解釋是,post請求第一次是預(yù)檢請求,Request Method: OPTIONS。
解決方法:

在webSecurityConfig里添加

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

就可以不攔截options請求了。

這里只給出了最主要的代碼,還有controller層的訪問權(quán)限設(shè)置,返回狀態(tài)碼,返回類定義等等。

看完這篇關(guān)于如何實(shí)現(xiàn)SpringBoot集成SpringSecurity和JWT做登陸鑒權(quán)的文章,如果覺得文章內(nèi)容寫得不錯的話,可以把它分享出去給更多人看到。

名稱欄目:如何實(shí)現(xiàn)SpringBoot集成SpringSecurity和JWT做登陸鑒權(quán)
分享網(wǎng)址:http://bm7419.com/article0/gijsio.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供服務(wù)器托管、靜態(tài)網(wǎng)站、建站公司動態(tài)網(wǎng)站、云服務(wù)器、響應(yīng)式網(wǎng)站

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

手機(jī)網(wǎng)站建設(shè)