<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Spring Boot 2 + Spring Security 5 + JWT 的 Restful簡易教程!

          共 43282字,需瀏覽 87分鐘

           ·

          2021-04-29 17:44

          準(zhǔn)備

          開始本教程的時(shí)候希望對(duì)下面知識(shí)點(diǎn)進(jìn)行粗略的了解。

          • 知道 JWT 的基本概念
          • 了解過 Spring Security

          本項(xiàng)目中 JWT 密鑰是使用用戶自己的登入密碼,這樣每一個(gè) token 的密鑰都不同,相對(duì)比較安全。

          大體思路:

          登入:

          1. POST 用戶名密碼到 \login
          2. 請(qǐng)求到達(dá) JwtAuthenticationFilter 中的 attemptAuthentication() 方法,獲取 request 中的 POST 參數(shù),包裝成一個(gè) UsernamePasswordAuthenticationToken 交付給 AuthenticationManagerauthenticate() 方法進(jìn)行鑒權(quán)。
          3. AuthenticationManager 會(huì)從 CachingUserDetailsService 中查找用戶信息,并且判斷賬號(hào)密碼是否正確。
          4. 如果賬號(hào)密碼正確跳轉(zhuǎn)到 JwtAuthenticationFilter 中的 successfulAuthentication() 方法,我們進(jìn)行簽名,生成 token 返回給用戶。
          5. 賬號(hào)密碼錯(cuò)誤則跳轉(zhuǎn)到 JwtAuthenticationFilter 中的 unsuccessfulAuthentication() 方法,我們返回錯(cuò)誤信息讓用戶重新登入。

          請(qǐng)求鑒權(quán):

          請(qǐng)求鑒權(quán)的主要思路是我們會(huì)從請(qǐng)求中的 Authorization 字段拿取 token,如果不存在此字段的用戶,Spring Security 會(huì)默認(rèn)會(huì)用 AnonymousAuthenticationToken() 包裝它,即代表匿名用戶。

          1. 任意請(qǐng)求發(fā)起
          2. 到達(dá) JwtAuthorizationFilter 中的 doFilterInternal() 方法,進(jìn)行鑒權(quán)。
          3. 如果鑒權(quán)成功我們把生成的 AuthenticationSecurityContextHolder.getContext().setAuthentication() 放入 Security,即代表鑒權(quán)完成。此處如何鑒權(quán)由我們自己代碼編寫,后序會(huì)詳細(xì)說明。

          準(zhǔn)備 pom.xml

          <?xml version="1.0" encoding="UTF-8"?>
          <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
              <modelVersion>4.0.0</modelVersion>
              <parent>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-parent</artifactId>
                  <version>2.1.7.RELEASE</version>
                  <relativePath/> <!-- lookup parent from repository -->
              </parent>
              <groupId>org.inlighting</groupId>
              <artifactId>spring-boot-security-jwt</artifactId>
              <version>0.0.1-SNAPSHOT</version>
              <name>spring-boot-security-jwt</name>
              <description>Demo project for Spring Boot</description>

              <properties>
                  <java.version>1.8</java.version>
              </properties>

              <dependencies>
                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-starter-security</artifactId>
                  </dependency>

                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-starter-web</artifactId>
                  </dependency>
                  <!-- JWT 支持 -->
                  <dependency>
                      <groupId>com.auth0</groupId>
                      <artifactId>java-jwt</artifactId>
                      <version>3.8.2</version>
                  </dependency>

                  <!-- cache 支持 -->
                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-starter-cache</artifactId>
                  </dependency>

                  <!-- cache 支持 -->
                  <dependency>
                      <groupId>org.ehcache</groupId>
                      <artifactId>ehcache</artifactId>
                  </dependency>

                  <!-- cache 支持 -->
                  <dependency>
                      <groupId>javax.cache</groupId>
                      <artifactId>cache-api</artifactId>
                  </dependency>

                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-starter-test</artifactId>
                      <scope>test</scope>
                  </dependency>

                  <dependency>
                      <groupId>org.springframework.security</groupId>
                      <artifactId>spring-security-test</artifactId>
                      <scope>test</scope>
                  </dependency>

                  <!-- ehcache 讀取 xml 配置文件使用 -->
                  <dependency>
                      <groupId>javax.xml.bind</groupId>
                      <artifactId>jaxb-api</artifactId>
                      <version>2.3.0</version>
                  </dependency>

                  <!-- ehcache 讀取 xml 配置文件使用 -->
                  <dependency>
                      <groupId>com.sun.xml.bind</groupId>
                      <artifactId>jaxb-impl</artifactId>
                      <version>2.3.0</version>
                  </dependency>

                  <!-- ehcache 讀取 xml 配置文件使用 -->
                  <dependency>
                      <groupId>com.sun.xml.bind</groupId>
                      <artifactId>jaxb-core</artifactId>
                      <version>2.3.0</version>
                  </dependency>

                  <!-- ehcache 讀取 xml 配置文件使用 -->
                  <dependency>
                      <groupId>javax.activation</groupId>
                      <artifactId>activation</artifactId>
                      <version>1.1.1</version>
                  </dependency>
              </dependencies>

              <build>
                  <plugins>
                      <plugin>
                          <groupId>org.springframework.boot</groupId>
                          <artifactId>spring-boot-maven-plugin</artifactId>
                      </plugin>
                  </plugins>
              </build>

          </project>

          pom.xml 配置文件這塊沒有什么好說的,主要說明下面的幾個(gè)依賴:

          <!-- ehcache 讀取 xml 配置文件使用 -->
          <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.0</version>
          </dependency>

          <!-- ehcache 讀取 xml 配置文件使用 -->
          <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>2.3.0</version>
          </dependency>

          <!-- ehcache 讀取 xml 配置文件使用 -->
          <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-core</artifactId>
            <version>2.3.0</version>
          </dependency>

          <!-- ehcache 讀取 xml 配置文件使用 -->
          <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
          </dependency>

          因?yàn)?ehcache 讀取 xml 配置文件時(shí)使用了這幾個(gè)依賴,而這幾個(gè)依賴從 JDK 9 開始時(shí)是選配模塊,所以高版本的用戶需要添加這幾個(gè)依賴才能正常使用。

          基礎(chǔ)工作準(zhǔn)備

          接下來準(zhǔn)備下幾個(gè)基礎(chǔ)工作,就是新建個(gè)實(shí)體、模擬個(gè)數(shù)據(jù)庫,寫個(gè) JWT 工具類這種基礎(chǔ)操作。

          UserEntity.java

          關(guān)于 role 為什么使用 GrantedAuthority 說明下:其實(shí)是為了簡化代碼,直接用了 Security 現(xiàn)成的 role 類,實(shí)際項(xiàng)目中我們肯定要自己進(jìn)行處理,將其轉(zhuǎn)換為 Security 的 role 類。

          public class UserEntity {

              public UserEntity(String username, String password, Collection<? extends GrantedAuthority> role) {
                  this.username = username;
                  this.password = password;
                  this.role = role;
              }

              private String username;

              private String password;

              private Collection<? extends GrantedAuthority> role;

              public String getUsername() {
                  return username;
              }

              public void setUsername(String username) {
                  this.username = username;
              }

              public String getPassword() {
                  return password;
              }

              public void setPassword(String password) {
                  this.password = password;
              }

              public Collection<? extends GrantedAuthority> getRole() {
                  return role;
              }

              public void setRole(Collection<? extends GrantedAuthority> role) {
                  this.role = role;
              }
          }

          ResponseEntity.java

          前后端分離為了方便前端我們要統(tǒng)一 json 的返回格式,所以自定義一個(gè) ResponseEntity.java。

          public class ResponseEntity {

              public ResponseEntity() {
              }

              public ResponseEntity(int status, String msg, Object data) {
                  this.status = status;
                  this.msg = msg;
                  this.data = data;
              }

              private int status;

              private String msg;

              private Object data;

              public int getStatus() {
                  return status;
              }

              public void setStatus(int status) {
                  this.status = status;
              }

              public String getMsg() {
                  return msg;
              }

              public void setMsg(String msg) {
                  this.msg = msg;
              }

              public Object getData() {
                  return data;
              }

              public void setData(Object data) {
                  this.data = data;
              }
          }

          Database.java

          這里我們使用一個(gè) HashMap 模擬了一個(gè)數(shù)據(jù)庫,密碼我已經(jīng)預(yù)先用 Bcrypt 加密過了,這也是 Spring Security 官方推薦的加密算法(MD5 加密已經(jīng)在 Spring Security 5 中被移除了,不安全)。

          用戶名密碼權(quán)限
          jackjack123 存 Bcrypt 加密后ROLE_USER
          dannydanny123 存 Bcrypt 加密后ROLE_EDITOR
          smithsmith123 存 Bcrypt 加密后ROLE_ADMIN
          @Component
          public class Database {
              private Map<String, UserEntity> data = null;
              
              public Map<String, UserEntity> getDatabase() {
                  if (data == null) {
                      data = new HashMap<>();

                      UserEntity jack = new UserEntity(
                              "jack",
                              "$2a$10$AQol1A.LkxoJ5dEzS5o5E.QG9jD.hncoeCGdVaMQZaiYZ98V/JyRq",
                              getGrants("ROLE_USER"));
                      UserEntity danny = new UserEntity(
                              "danny",
                              "$2a$10$8nMJR6r7lvh9H2INtM2vtuA156dHTcQUyU.2Q2OK/7LwMd/I.HM12",
                              getGrants("ROLE_EDITOR"));
                      UserEntity smith = new UserEntity(
                              "smith",
                              "$2a$10$E86mKigOx1NeIr7D6CJM3OQnWdaPXOjWe4OoRqDqFgNgowvJW9nAi",
                              getGrants("ROLE_ADMIN"));
                      data.put("jack", jack);
                      data.put("danny", danny);
                      data.put("smith", smith);
                  }
                  return data;
              }
              
              private Collection<GrantedAuthority> getGrants(String role) {
                  return AuthorityUtils.commaSeparatedStringToAuthorityList(role);
              }
          }

          UserService.java

          這里再模擬一個(gè) service,主要就是模仿數(shù)據(jù)庫的操作。

          @Service
          public class UserService {

              @Autowired
              private Database database;

              public UserEntity getUserByUsername(String username) {
                  return database.getDatabase().get(username);
              }
          }

          JwtUtil.java

          自己編寫的一個(gè)工具類,主要負(fù)責(zé) JWT 的簽名和鑒權(quán)。

          public class JwtUtil {

              // 過期時(shí)間5分鐘
              private final static long EXPIRE_TIME = 5 * 60 * 1000;

              /**
               * 生成簽名,5min后過期
               * @param username 用戶名
               * @param secret 用戶的密碼
               * @return 加密的token
               */
              public static String sign(String username, String secret) {
                  Date expireDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
                  try {
                      Algorithm algorithm = Algorithm.HMAC256(secret);
                      return JWT.create()
                              .withClaim("username", username)
                              .withExpiresAt(expireDate)
                              .sign(algorithm);
                  } catch (Exception e) {
                      return null;
                  }
              }

              /**
               * 校驗(yàn)token是否正確
               * @param token 密鑰
               * @param secret 用戶的密碼
               * @return 是否正確
               */
              public static boolean verify(String token, String username, String secret) {
                  try {
                      Algorithm algorithm = Algorithm.HMAC256(secret);
                      JWTVerifier verifier = JWT.require(algorithm)
                              .withClaim("username", username)
                              .build();
                      DecodedJWT jwt = verifier.verify(token);
                      return true;
                  } catch (Exception e) {
                      return false;
                  }
              }

              /**
               * 獲得token中的信息無需secret解密也能獲得
               * @return token中包含的用戶名
               */
              public static String getUsername(String token) {
                  try {
                      DecodedJWT jwt = JWT.decode(token);
                      return jwt.getClaim("username").asString();
                  } catch (JWTDecodeException e) {
                      return null;
                  }
              }
          }

          Spring Security 改造

          登入這塊,我們使用自定義的 JwtAuthenticationFilter 來進(jìn)行登入。

          請(qǐng)求鑒權(quán),我們使用自定義的 JwtAuthorizationFilter 來處理。

          也許大家覺得兩個(gè)單詞長的有點(diǎn)像,??。

          UserDetailsServiceImpl.java

          我們首先實(shí)現(xiàn)官方的 UserDetailsService 接口,這里主要負(fù)責(zé)一個(gè)從數(shù)據(jù)庫拿數(shù)據(jù)的操作。

          @Service
          public class UserDetailsServiceImpl implements UserDetailsService {

              @Autowired
              private UserService userService;

              @Override
              public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                  UserEntity userEntity = userService.getUserByUsername(username);
                  if (userEntity == null) {
                      throw new UsernameNotFoundException("This username didn't exist.");
                  }
                  return new User(userEntity.getUsername(), userEntity.getPassword(), userEntity.getRole());
              }
          }

          后序我們還需要對(duì)其進(jìn)行緩存改造,不然每次請(qǐng)求都要從數(shù)據(jù)庫拿一次數(shù)據(jù)鑒權(quán),對(duì)數(shù)據(jù)庫壓力太大了。

          JwtAuthenticationFilter.java

          這個(gè)過濾器主要處理登入操作,我們繼承了 UsernamePasswordAuthenticationFilter,這樣能大大簡化我們的工作量。

          public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

              /*
              過濾器一定要設(shè)置 AuthenticationManager,所以此處我們這么編寫,這里的 AuthenticationManager
              我會(huì)從 Security 配置的時(shí)候傳入
              */
              public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
                  /*
                  運(yùn)行父類 UsernamePasswordAuthenticationFilter 的構(gòu)造方法,能夠設(shè)置此濾器指定
                  方法為 POST [\login]
                  */
                  super();
                  setAuthenticationManager(authenticationManager);
              }

              @Override
              public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
                  // 從請(qǐng)求的 POST 中拿取 username 和 password 兩個(gè)字段進(jìn)行登入
                  String username = request.getParameter("username");
                  String password = request.getParameter("password");
                  UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
                  // 設(shè)置一些客戶 IP 啥信息,后面想用的話可以用,雖然沒啥用
                  setDetails(request, token);
                  // 交給 AuthenticationManager 進(jìn)行鑒權(quán)
                  return getAuthenticationManager().authenticate(token);
              }

              /*
              鑒權(quán)成功進(jìn)行的操作,我們這里設(shè)置返回加密后的 token
              */
              @Override
              protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
                  handleResponse(request, response, authResult, null);
              }

              /*
              鑒權(quán)失敗進(jìn)行的操作,我們這里就返回 用戶名或密碼錯(cuò)誤 的信息
              */
              @Override
              protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
                  handleResponse(request, response, null, failed);
              }

              private void handleResponse(HttpServletRequest request, HttpServletResponse response, Authentication authResult, AuthenticationException failed) throws IOException, ServletException {
                  ObjectMapper mapper = new ObjectMapper();
                  ResponseEntity responseEntity = new ResponseEntity();
                  response.setHeader("Content-Type""application/json;charset=UTF-8");
                  if (authResult != null) {
                      // 處理登入成功請(qǐng)求
                      User user = (User) authResult.getPrincipal();
                      String token = JwtUtil.sign(user.getUsername(), user.getPassword());
                      responseEntity.setStatus(HttpStatus.OK.value());
                      responseEntity.setMsg("登入成功");
                      responseEntity.setData("Bearer " + token);
                      response.setStatus(HttpStatus.OK.value());
                      response.getWriter().write(mapper.writeValueAsString(responseEntity));
                  } else {
                      // 處理登入失敗請(qǐng)求
                      responseEntity.setStatus(HttpStatus.BAD_REQUEST.value());
                      responseEntity.setMsg("用戶名或密碼錯(cuò)誤");
                      responseEntity.setData(null);
                      response.setStatus(HttpStatus.BAD_REQUEST.value());
                      response.getWriter().write(mapper.writeValueAsString(responseEntity));
                  }
              }
          }

          private void handleResponse() 此處處理的方法不是很好,我的想法是跳轉(zhuǎn)到控制器中進(jìn)行處理,但是這樣鑒權(quán)成功的 token 帶不過去,所以先這么寫了,有點(diǎn)復(fù)雜。

          JwtAuthorizationFilter.java

          這個(gè)過濾器處理每個(gè)請(qǐng)求鑒權(quán),我們選擇繼承 BasicAuthenticationFilter ,考慮到 Basic 認(rèn)證和 JWT 比較像,就選擇了它。

          public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

              private UserDetailsService userDetailsService;

              // 會(huì)從 Spring Security 配置文件那里傳過來
              public JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserDetailsService userDetailsService) {
                  super(authenticationManager);
                  this.userDetailsService = userDetailsService;
              }

              @Override
              protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
                  // 判斷是否有 token,并且進(jìn)行認(rèn)證
                  Authentication token = getAuthentication(request);
                  if (token == null) {
                      chain.doFilter(request, response);
                      return;
                  }
                  // 認(rèn)證成功
                  SecurityContextHolder.getContext().setAuthentication(token);
                  chain.doFilter(request, response);
              }

              private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
                  String header = request.getHeader("Authorization");
                  if (header == null || ! header.startsWith("Bearer ")) {
                      return null;
                  }

                  String token = header.split(" ")[1];
                  String username = JwtUtil.getUsername(token);
                  UserDetails userDetails = null;
                  try {
                      userDetails = userDetailsService.loadUserByUsername(username);
                  } catch (UsernameNotFoundException e) {
                      return null;
                  }
                  if (! JwtUtil.verify(token, username, userDetails.getPassword())) {
                      return null;
                  }
                  return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
              }
          }

          SecurityConfiguration.java

          此處我們進(jìn)行 Security 的配置,并且實(shí)現(xiàn)緩存功能。緩存這塊我們使用官方現(xiàn)成的 CachingUserDetailsService ,唯獨(dú)的缺點(diǎn)就是它沒有 public 方法,我們不能正常實(shí)例化,需要曲線救國,下面代碼也有詳細(xì)說明。

          // 開啟 Security
          @EnableWebSecurity
          // 開啟注解配置支持
          @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
          public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

              @Autowired
              private UserDetailsServiceImpl userDetailsServiceImpl;

              // Spring Boot 的 CacheManager,這里我們使用 JCache
              @Autowired
              private CacheManager cacheManager;

              @Override
              protected void configure(HttpSecurity http) throws Exception {
                  // 開啟跨域
                  http.cors()
                          .and()
                          // security 默認(rèn) csrf 是開啟的,我們使用了 token ,這個(gè)也沒有什么必要了
                          .csrf().disable()
                          .authorizeRequests()
                          // 默認(rèn)所有請(qǐng)求通過,但是我們要在需要權(quán)限的方法加上安全注解,這樣比寫死配置靈活很多
                          .anyRequest().permitAll()
                          .and()
                          // 添加自己編寫的兩個(gè)過濾器
                          .addFilter(new JwtAuthenticationFilter(authenticationManager()))
                          .addFilter(new JwtAuthorizationFilter(authenticationManager(), cachingUserDetailsService(userDetailsServiceImpl)))
                          // 前后端分離是 STATELESS,故 session 使用該策略
                          .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
              }

              // 此處配置 AuthenticationManager,并且實(shí)現(xiàn)緩存
              @Override
              protected void configure(AuthenticationManagerBuilder auth) throws Exception {
                  // 對(duì)自己編寫的 UserDetailsServiceImpl 進(jìn)一步包裝,實(shí)現(xiàn)緩存
                  CachingUserDetailsService cachingUserDetailsService = cachingUserDetailsService(userDetailsServiceImpl);
                  // jwt-cache 我們?cè)?nbsp;ehcache.xml 配置文件中有聲明
                  UserCache userCache = new SpringCacheBasedUserCache(cacheManager.getCache("jwt-cache"));
                  cachingUserDetailsService.setUserCache(userCache);
                  /*
                  security 默認(rèn)鑒權(quán)完成后會(huì)把密碼抹除,但是這里我們使用用戶的密碼來作為 JWT 的生成密鑰,
                  如果被抹除了,在對(duì) JWT 進(jìn)行簽名的時(shí)候就拿不到用戶密碼了,故此處關(guān)閉了自動(dòng)抹除密碼。
                   */
                  auth.eraseCredentials(false);
                  auth.userDetailsService(cachingUserDetailsService);
              }

              @Bean
              public PasswordEncoder passwordEncoder() {
                  return new BCryptPasswordEncoder();
              }

              /*
              此處我們實(shí)現(xiàn)緩存的時(shí)候,我們使用了官方現(xiàn)成的 CachingUserDetailsService ,但是這個(gè)類的構(gòu)造方法不是 public 的,
              我們不能夠正常實(shí)例化,所以在這里進(jìn)行曲線救國。
               */
              private CachingUserDetailsService cachingUserDetailsService(UserDetailsServiceImpl delegate) {

                  Constructor<CachingUserDetailsService> ctor = null;
                  try {
                      ctor = CachingUserDetailsService.class.getDeclaredConstructor(UserDetailsService.class);
                  } catch (NoSuchMethodException e) {
                      e.printStackTrace();
                  }
                  Assert.notNull(ctor, "CachingUserDetailsService constructor is null");
                  ctor.setAccessible(true);
                  return BeanUtils.instantiateClass(ctor, delegate);
              }
          }

          Ehcache 配置

          Ehcache 3 開始,統(tǒng)一使用了 JCache,就是 JSR107 標(biāo)準(zhǔn),網(wǎng)上很多教程都是基于 Ehcache 2 的,所以大家可能在參照網(wǎng)上的教程會(huì)遇到很多坑。

          JSR107:emm,其實(shí) JSR107 是一種緩存標(biāo)準(zhǔn),各個(gè)框架只要遵守這個(gè)標(biāo)準(zhǔn),就是現(xiàn)實(shí)大一統(tǒng)。差不多就是我不需要更改系統(tǒng)代碼,也能隨意更換底層的緩存系統(tǒng)。

          在 resources 目錄下創(chuàng)建 ehcache.xml 文件:

          <ehcache:config
                  xmlns:ehcache="http://www.ehcache.org/v3"
                  xmlns:jcache="http://www.ehcache.org/v3/jsr107">

              <ehcache:cache alias="jwt-cache">
                  <!-- 我們使用用戶名作為緩存的 key,故使用 String -->
                  <ehcache:key-type>java.lang.String</ehcache:key-type>
                  <ehcache:value-type>org.springframework.security.core.userdetails.User</ehcache:value-type>
                  <ehcache:expiry>
                      <ehcache:ttl unit="days">1</ehcache:ttl>
                  </ehcache:expiry>
                  <!-- 緩存實(shí)體的數(shù)量 -->
                  <ehcache:heap unit="entries">2000</ehcache:heap>
              </ehcache:cache>

          </ehcache:config>

          application.properties 中開啟緩存支持:

          spring.cache.type=jcache
          spring.cache.jcache.config=classpath:ehcache.xml

          統(tǒng)一全局異常

          我們要把異常的返回形式也統(tǒng)一了,這樣才能方便前端的調(diào)用。

          我們平常會(huì)使用 @RestControllerAdvice 來統(tǒng)一異常,但是它只能管理 Controller 層面拋出的異常。Security 中拋出的異常不會(huì)抵達(dá) Controller,無法被 @RestControllerAdvice 捕獲,故我們還要改造 ErrorController 。

          @RestController
          public class CustomErrorController implements ErrorController {

              @Override
              public String getErrorPath() {
                  return "/error";
              }

              @RequestMapping("/error")
              public ResponseEntity handleError(HttpServletRequest request, HttpServletResponse response) {
                  return new ResponseEntity(response.getStatus(), (String) request.getAttribute("javax.servlet.error.message"), null);
              }
          }

          測(cè)試

          寫個(gè)控制器試試,大家也可以參考我控制器里面獲取用戶信息的方式,推薦使用 @AuthenticationPrincipal 這個(gè)注解?。?!

          @RestController
          public class MainController {

              // 任何人都可以訪問,在方法中判斷用戶是否合法
              @GetMapping("everyone")
              public ResponseEntity everyone() {
                  Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                  if (! (authentication instanceof AnonymousAuthenticationToken)) {
                      // 登入用戶
                      return new ResponseEntity(HttpStatus.OK.value(), "You are already login", authentication.getPrincipal());
                  } else {
                      return new ResponseEntity(HttpStatus.OK.value(), "You are anonymous", null);
                  }
              }

              @GetMapping("user")
              @PreAuthorize("hasAuthority('ROLE_USER')")
              public ResponseEntity user(@AuthenticationPrincipal UsernamePasswordAuthenticationToken token) {
                  return new ResponseEntity(HttpStatus.OK.value(), "You are user", token);
              }

              @GetMapping("admin")
              @IsAdmin
              public ResponseEntity admin(@AuthenticationPrincipal UsernamePasswordAuthenticationToken token) {
                  return new ResponseEntity(HttpStatus.OK.value(), "You are admin", token);
              }
          }

          我這里還使用了 @IsAdmin 注解,@IsAdmin 注解如下:

          @Target({ElementType.METHOD, ElementType.TYPE})
          @Retention(RetentionPolicy.RUNTIME)
          @PreAuthorize("hasAnyRole('ROLE_ADMIN')")
          public @interface IsAdmin {
          }

          這樣能省去每次編寫一長串的 @PreAuthorize() ,而且更加直觀。

          FAQ

          如何解決JWT過期問題?

          我們可以在 JwtAuthorizationFilter 中加點(diǎn)料,如果用戶快過期了,返回個(gè)特別的狀態(tài)碼,前端收到此狀態(tài)碼去訪問 GET /re_authentication 攜帶老的 token 重新拿一個(gè)新的 token 即可。

          如何作廢已頒發(fā)未過期的 token?

          個(gè)人的想法是把每次生成的 token 放入緩存中,每次請(qǐng)求都從緩存里拿,如果沒有則代表此緩存報(bào)廢。

          項(xiàng)目地址及來源:https://github.com/Smith-Cruise/Spring-Boot-Security-JWT-SPA

          最近給大家找了  JVM學(xué)習(xí)視頻


          資源,怎么領(lǐng)???


          掃二維碼,加我微信,回復(fù):JVM

           注意,不要亂回復(fù) 

          沒錯(cuò),不是機(jī)器人
          記得一定要等待,等待才有好東西



          瀏覽 50
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩A片免费在线观看 | 久久,丁香,婷婷,小说 | 综合网第一页 | 日日嗨夜夜嗨一区二区 | 艹逼美女a级毛片 |