1. 程式人生 > >Spring Boot中使用使用Spring Security和JWT

Spring Boot中使用使用Spring Security和JWT

 目標

1.Token鑑權

2.Restful API

3.Spring Security+JWT

開始

自行新建Spring Boot工程

引入相關依賴

複製程式碼
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>1.5.9.RELEASE</version>
</dependency>
<dependency
> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
複製程式碼

User類

非常簡單的使用者模型,將許可權整合到了使用者類中。

複製程式碼
pacage com.domain
/**
* 使用者模型 * * @author hackyo * Created on 2017/12/3 11:53. */ public class User {
</span><span style="color: #0000ff">private</span><span style="color: #000000"> String id;
</span><span style="color: #0000ff">private</span><span style="color: #000000"> String username;
</span><span style="color: #0000ff">private</span><span style="color: #000000"> String password;
</span><span style="color: #0000ff">private</span> List&lt;String&gt;<span style="color: #000000"> roles;

......
省略get、set方法
......

}

複製程式碼

IUserRepository類

需實現對使用者表的增刪改查,此處可採用任意資料庫,具體實現自行編寫。

複製程式碼
package com.dao
/**
* 使用者表操作介面 * * @author hackyo * Created on 2017/12/3 11:53. */ @Component public interface IUserRepository{
</span><span style="color: #008000">/**</span><span style="color: #008000">
 * 通過使用者名稱查詢使用者
 *
 * </span><span style="color: #808080">@param</span><span style="color: #008000"> username 使用者名稱
 * </span><span style="color: #808080">@return</span><span style="color: #008000"> 使用者資訊
 </span><span style="color: #008000">*/</span><span style="color: #000000">
User findByUsername(String username);

}

複製程式碼

JwtUser類

安全模組的使用者模型

複製程式碼
package com.security;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**

  • 安全使用者模型

  • @author hackyo

  • Created on 2017/12/8 9:20.
    */
    public class JwtUser implements UserDetails {

    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;

    JwtUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
    this.username = username;
    this.password = password;
    this.authorities = authorities;
    }

    @Override
    public String getUsername() {
    return username;
    }

    @JsonIgnore
    @Override
    public String getPassword() {
    return password;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    return authorities;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
    return true;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
    return true;
    }

    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
    return true;
    }

    @JsonIgnore
    @Override
    public boolean isEnabled() {
    return true;
    }

}

複製程式碼

 JwtTokenUtil類

Token工具類

這裡設定了金鑰為aaaaaaaa,有效期為2592000秒

複製程式碼
package com.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**

  • JWT工具類

  • @author hackyo

  • Created on 2017/12/8 9:20.
    */
    @Component
    public class JwtTokenUtil implements Serializable {

    /**

    • 金鑰
      */
      private final String secret = “aaaaaaaa”;

    /**

    • 從資料宣告生成令牌
    • @param claims 資料宣告
    • @return 令牌
      */
      private String generateToken(Map<String, Object> claims) {
      Date expirationDate
      = new Date(System.currentTimeMillis() + 2592000L * 1000);
      return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact();
      }

    /**

    • 從令牌中獲取資料宣告
    • @param token 令牌
    • @return 資料宣告
      */
      private Claims getClaimsFromToken(String token) {
      Claims claims;
      try {
      claims
      = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
      }
      catch (Exception e) {
      claims
      = null;
      }
      return claims;
      }

    /**

    • 生成令牌
    • @param userDetails 使用者
    • @return 令牌
      */
      public String generateToken(UserDetails userDetails) {
      Map
      <String, Object> claims = new HashMap<>(2);
      claims.put(
      “sub”, userDetails.getUsername());
      claims.put(
      “created”, new Date());
      return generateToken(claims);
      }

    /**

    • 從令牌中獲取使用者名稱
    • @param token 令牌
    • @return 使用者名稱
      */
      public String getUsernameFromToken(String token) {
      String username;
      try {
      Claims claims
      = getClaimsFromToken(token);
      username
      = claims.getSubject();
      }
      catch (Exception e) {
      username
      = null;
      }
      return username;
      }

    /**

    • 判斷令牌是否過期
    • @param token 令牌
    • @return 是否過期
      */
      public Boolean isTokenExpired(String token) {
      try {
      Claims claims
      = getClaimsFromToken(token);
      Date expiration
      = claims.getExpiration();
      return expiration.before(new Date());
      }
      catch (Exception e) {
      return false;
      }
      }

    /**

    • 重新整理令牌
    • @param token 原令牌
    • @return 新令牌
      */
      public String refreshToken(String token) {
      String refreshedToken;
      try {
      Claims claims
      = getClaimsFromToken(token);
      claims.put(
      “created”, new Date());
      refreshedToken
      = generateToken(claims);
      }
      catch (Exception e) {
      refreshedToken
      = null;
      }
      return refreshedToken;
      }

    /**

    • 驗證令牌
    • @param token 令牌
    • @param userDetails 使用者
    • @return 是否有效
      */
      public Boolean validateToken(String token, UserDetails userDetails) {
      JwtUser user
      = (JwtUser) userDetails;
      String username
      = getUsernameFromToken(token);
      return (username.equals(user.getUsername()) && !isTokenExpired(token));
      }

}

複製程式碼

 JwtUserDetailsServiceImpl類

使用者驗證方法類

複製程式碼
package com.security;

import com.safepass.dao.IUserRepository;
import com.safepass.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.stream.Collectors;

/**

  • 使用者驗證方法

  • @author hackyo

  • Created on 2017/12/8 9:18.
    */
    @Service
    public class JwtUserDetailsServiceImpl implements UserDetailsService {

    private IUserRepository userRepository;

    @Autowired
    public JwtUserDetailsServiceImpl(IUserRepository userRepository) {
    this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user
    = userRepository.findByUsername(username);
    if (user == null) {
    throw new UsernameNotFoundException(String.format(“No user found with username ‘%s’.”, username));
    }
    else {
    return new JwtUser(user.getUsername(), user.getPassword(), user.getRoles().stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
    }
    }

}

複製程式碼

JwtAuthenticationTokenFilter類

Token過濾器實現

複製程式碼
package com.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**

  • Token過濾器

  • @author hackyo

  • Created on 2017/12/8 9:28.
    */
    @Component
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private UserDetailsService userDetailsService;
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    public JwtAuthenticationTokenFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil) {
    this.userDetailsService = userDetailsService;
    this.jwtTokenUtil = jwtTokenUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
    String authHeader
    = request.getHeader(“Authorization”);
    String tokenHead
    = "Bearer ";
    if (authHeader != null && authHeader.startsWith(tokenHead)) {
    String authToken
    = authHeader.substring(tokenHead.length());
    String username
    = jwtTokenUtil.getUsernameFromToken(authToken);
    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));
    SecurityContextHolder.getContext().setAuthentication(authentication);
    }
    }
    }
    chain.doFilter(request, response);
    }

}

複製程式碼

EntryPointUnauthorizedHandler類

自定義了身份驗證失敗的返回值

複製程式碼
package com.security;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**

  • 自定401返回值

  • @author hackyo

  • Created on 2017/12/9 20:10.
    */
    @Component
    public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
    response.setHeader(
    “Access-Control-Allow-Origin”, “*”);
    response.setStatus(
    401);
    }

}

複製程式碼

RestAccessDeniedHandler類

自定了許可權不足的返回值

複製程式碼
package com.security;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**

  • 自定403返回值

  • @author hackyo

  • Created on 2017/12/9 20:10.
    */
    @Component
    public class RestAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) {
    response.setHeader(
    “Access-Control-Allow-Origin”, “*”);
    response.setStatus(
    403);
    }

}

複製程式碼

WebSecurityConfig類

安全配置類

這裡設定了禁止訪問所有地址,除了用於驗證身份的/user/**地址

同時密碼的加密方式為BCrypt

複製程式碼
package com.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**

  • 安全模組配置

  • @author hackyo

  • Created on 2017/12/8 9:15.
    */
    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled
    = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private UserDetailsService userDetailsService;
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;
    private RestAccessDeniedHandler restAccessDeniedHandler;
    private PasswordEncoder passwordEncoder;

    @Autowired
    public WebSecurityConfig(UserDetailsService userDetailsService, JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter, EntryPointUnauthorizedHandler entryPointUnauthorizedHandler, RestAccessDeniedHandler restAccessDeniedHandler) {
    this.userDetailsService = userDetailsService;
    this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
    this.entryPointUnauthorizedHandler = entryPointUnauthorizedHandler;
    this.restAccessDeniedHandler = restAccessDeniedHandler;
    this.passwordEncoder = new BCryptPasswordEncoder();
    }

    @Autowired
    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
    authenticationManagerBuilder.userDetailsService(
    this.userDetailsService).passwordEncoder(passwordEncoder);
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    .and().authorizeRequests()
    .antMatchers(HttpMethod.OPTIONS,
    “/").permitAll()
    .antMatchers(
    "/user/
    ).permitAll()
    .anyRequest().authenticated()
    .and().headers().cacheControl();
    httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.
    class);
    httpSecurity.exceptionHandling().authenticationEntryPoint(entryPointUnauthorizedHandler).accessDeniedHandler(restAccessDeniedHandler);

    }

}

複製程式碼

IUserService類

定義使用者的基本操作

複製程式碼
package com.service;

import com.domain.User;

/**

  • 使用者操作介面

  • @author hackyo

  • Created on 2017/12/3 11:53.
    */
    public interface IUserService {

    /**

    • 使用者登入
    • @param username 使用者名稱
    • @param password 密碼
    • @return 操作結果
      */
      String login(String username, String password);

    /**

    • 使用者註冊
    • @param user 使用者資訊
    • @return 操作結果
      */
      String register(User user);

    /**

    • 重新整理金鑰
    • @param oldToken 原金鑰
    • @return 新金鑰
      */
      String refreshToken(String oldToken);

}

複製程式碼

UserServiceImpl類

IUserService的實現類,註冊時會將使用者許可權設定為ROLE_USER,同時將密碼使用BCrypt加密

複製程式碼
package com.service.impl;

import com.dao.IUserRepository;
import com.domain.User;
import com.security.JwtTokenUtil;
import com.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**

  • 使用者操作介面實現

  • @author hackyo

  • Created on 2017/12/3 11:53.
    */
    @Service
    public class UserServiceImpl implements IUserService {

    private AuthenticationManager authenticationManager;
    private UserDetailsService userDetailsService;
    private JwtTokenUtil jwtTokenUtil;
    private IUserRepository userRepository;

    @Autowired
    public UserServiceImpl(AuthenticationManager authenticationManager, UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, IUserRepository userRepository) {
    this.authenticationManager = authenticationManager;
    this.userDetailsService = userDetailsService;
    this.jwtTokenUtil = jwtTokenUtil;
    this.userRepository = userRepository;
    }

    @Override
    public String login(String username, String password) {
    UsernamePasswordAuthenticationToken upToken
    = new UsernamePasswordAuthenticationToken(username, password);
    Authentication authentication
    = authenticationManager.authenticate(upToken);
    SecurityContextHolder.getContext().setAuthentication(authentication);
    UserDetails userDetails
    = userDetailsService.loadUserByUsername(username);
    return jwtTokenUtil.generateToken(userDetails);
    }

    @Override
    public String register(User user) {
    String username
    = user.getUsername();
    if (userRepository.findByUsername(username) != null) {
    return “使用者已存在”;
    }
    BCryptPasswordEncoder encoder
    = new BCryptPasswordEncoder();
    String rawPassword
    = user.getPassword();
    user.setPassword(encoder.encode(rawPassword));
    List
    <String> roles = new ArrayList<>();
    roles.add(
    “ROLE_USER”);
    user.setRoles(roles);
    userRepository.insert(user);
    return “success”;
    }

    @Override
    public String refreshToken(String oldToken) {
    String token
    = oldToken.substring("Bearer ".length());
    if (!jwtTokenUtil.isTokenExpired(token)) {
    return jwtTokenUtil.refreshToken(token);
    }
    return “error”;
    }

}

複製程式碼

UserController類

控制器,控制訪問

複製程式碼
package com.controller;

import com.domain.User;
import com.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.*;

/**

  • 使用者管理Controller

  • @author hackyo

  • Created on 2017/12/3 11:53.
    */
    @CrossOrigin
    @RestController
    @RequestMapping(value
    = “/user”, produces = “text/html;charset=UTF-8”)
    public class UserController {

    private IUserService userService;

    @Autowired
    public UserController(IUserService userService) {
    this.userService = userService;
    }

    /**

    • 使用者登入
    • @param username 使用者名稱
    • @param password 密碼
    • @return 操作結果
    • @throws AuthenticationException 錯誤資訊
      */
      @PostMapping(value
      = “/login”, params = {“username”, “password”})
      public String getToken(String username, String password) throws AuthenticationException {
      return userService.login(username, password);
      }

    /**

    • 使用者註冊
    • @param user 使用者資訊
    • @return 操作結果
    • @throws AuthenticationException 錯誤資訊
      */
      @PostMapping(value
      = “/register”)
      public String register(User user) throws AuthenticationException {
      return userService.register(user);
      }

    /**

    • 重新整理金鑰
    • @param authorization 原金鑰
    • @return 新金鑰
    • @throws AuthenticationException 錯誤資訊
      */
      @GetMapping(value
      = “/refreshToken”)
      public String refreshToken(@RequestHeader String authorization) throws AuthenticationException {
      return userService.refreshToken(authorization);
      }

}

複製程式碼

使用

只需要在方法或類上加註解即可實現賬號控制

例如,我們想控制該方法只允許使用者本人使用,#號表示方法的引數,可以在引數中加上@P('name')來指定名稱,同時也可直接使用模型,如user.username等

總之,其中可以寫入任何Spring EL

@PreAuthorize("#username == authentication.name")
    @GetMapping(value = "/getInfo")
    public String getInfo(String username) {
        return JSON.toJSONString(userService.getInfo(username));
    }

另外也可以自定義控制註解,使用@PostFilter註解,並實現hasPermission類即可,同時需要在WebSecurityConfigurerAdapter中開啟。

測試

執行程式後,我們使用Postman進行測試

1.註冊

URL:http://localhost:8080/user/register

引數:username、password

返回success即為成功

 2.登入

URL:http://localhost:8080/user/login

引數:username、password

可以看到伺服器將我們的Token返回了

 3.重新整理Token

URL(GET方法):http://localhost:8080/user/refreshToken

引數:在Header中加入登入時返回的Token,注意,需要在Token前加上“Bearer ”,最後有個空格

Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE1MTMzMTE1NjMsInN1YiI6IjEyMyIsImNyZWF0ZWQiOjE1MTI3MDY3NjM3NjB9.baiY8QcbJgq4FQMC2piN1smbW57WjDDTiRVIL9hJeC_DcPgcyJweWqkS6g7825mPKFlByuUx7XN8nUOIszDVcw

可以看到伺服器給我們返回了新的Token,如果我們不加上Token的話,將無法訪問

參考: