SpringBoot+Security+JWT進階:二、自定義認證實踐

title: SpringBoot+Security+JWT進階:二、自定義認證實踐
date: 2019-07-04
author: maxzhao
tags:
- JAVA
- SpringBoot
- Security
- JWT
- Authentication
categories:
- SpringBoot
- Security+JWT

前言

閱讀下文要了解

注釋是按照我初學的時候寫的,如果有錯誤或者不清楚的地方,希望大家能給我指出。

思路

  1. 構建
  2. 導入 security 、 jwt 依賴
  3. 用戶的驗證(service 、 dao 、model)
  4. 實現UserDetailsServiceUserDetails接口
  5. 可選:實現PasswordEncoder 接口(密碼加密)
  6. 驗證用戶登錄信息、用戶權限的攔截器
  7. security 配置
  8. 登錄認證 API

類圖(參考)

構建

略...

導入 security 、 jwt 依賴

略....

用戶的驗證(service 、 dao 、model)

就是查詢用戶所有庫的邏輯代碼

略....

實現UserDetailsServiceUserDetails接口

UserDetailsService

/**
 * 加載特定于用戶的數據的核心接口。
 * 它作為用戶DAO在整個框架中使用,是DaoAuthenticationProvider使用的策略。
 * 該接口只需要一個只讀方法,這簡化了對新數據訪問策略的支持。
 *
 * @author maxzhao
 */
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
    private static final Logger logger = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
    /**
     * 用戶操作服務
     */
    @Resource(name = "appUserService")
    private AppUserService appUserService;

    /**
     * 用戶角色服務
     */
    @Resource(name = "appRoleService")
    private AppRoleService appRoleService;
//todo https://segmentfault.com/a/1190000013057238#articleHeader7

    /**
     * 根據用戶登錄名定位用戶。
     *
     * @param loginName
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String loginName) throws UsernameNotFoundException {

        UserDetails userDetails = null;
        try {
            AppUser appUser = appUserService.findByLoginName(loginName);
            if (appUser != null) {
                // 查詢當前用戶的權限
                List<AppRole> appRoleList = appRoleService.findByUserId(appUser.getId());
                Collection<GrantedAuthority> authorities = new ArrayList<>();
                for (AppRole appRole : appRoleList) {
                    SimpleGrantedAuthority grant = new SimpleGrantedAuthority(appRole.getConstName());
                    authorities.add(grant);
                }
                //封裝自定義UserDetails類
                userDetails = new UserDetailsImpl(appUser, authorities);
            } else {
                /**
                 * UsernameNotFoundException 不能拋出問題不能獲取 問題解決
                 * DaoAuthenticationProvider類的retrieveUser 中會重寫輸出的異常
                 * 在這個方法會捕獲 UsernameNotFoundException 異常,會執行到父抽象類 AbstractUserDetailsAuthenticationProvider的authenticate方法
                 * 解決方案一:自定義異常
                 * 解決方案二:設置 AbstractUserDetailsAuthenticationProvider 的 hideUserNotFoundExceptions 屬性為 true
                 * 解決方案三:直接拋出 BadCredentialsException (最終返回的錯誤,一般為 message ,拋出的錯誤只為開發識別)
                 * 解決方案四:自定義認證,實現 AuthenticationProvider 接口
                 */
                throw new BadCredentialsException("該用戶不存在!");
            }
        } catch (Exception e) {
            logger.error(e.getMessage());
        }
        return userDetails;
    }
}

UserDetails

/**
 * 自定義用戶身份信息
 * 提供核心用戶信息。
 * 出于安全目的,Spring Security不直接使用實現。它們只是存儲用戶信息,這些信息稍后封裝到身份驗證對象中。這允許將非安全相關的用戶信息(如電子郵件地址、電話號碼等)存儲在一個方便的位置。
 * 具體實現必須特別注意,以確保每個方法的非空契約都得到了執行。有關參考實現(您可能希望在代碼中對其進行擴展或使用),請參見User。
 *
 * @author maxzhao
 * @date 2019-05-22
 */
public class UserDetailsImpl implements UserDetails {
    private static final long serialVersionUID = 1L;
    /**
     * 用戶信息
     */
    private AppUser appUser;
    /**
     * 用戶角色
     */
    private Collection<? extends GrantedAuthority> authorities;

    public UserDetailsImpl(AppUser appUser, Collection<? extends GrantedAuthority> authorities) {
        super();
        this.appUser = appUser;
        this.authorities = authorities;
    }

    /**
     * 返回用戶所有角色的封裝,一個Role對應一個GrantedAuthority
     *
     * @return 返回授予用戶的權限。
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    /*    Collection<GrantedAuthority> authorities = new ArrayList<>();
        String username = this.getUsername();
        if (username != null) {
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(username);
            authorities.add(authority);
        }*/
        return authorities;
    }

    /**
     * 返回用于驗證用戶身份的密碼。
     *
     * @return Returns the password used to authenticate the user.
     */
    @Override
    public String getPassword() {
        return appUser.getPassword();
    }

    /**
     * @return
     */
    @Override
    public String getUsername() {
        return appUser.getLoginName();
    }

    /**
     * 判斷賬號是否已經過期,默認沒有過期
     *
     * @return true 沒有過期
     */
    @Override
    public boolean isAccountNonExpired() {
        return appUser.getExpiration() == null || appUser.getExpiration().before(new Date());
    }

    /**
     * 判斷賬號是否被鎖定,默認沒有鎖定
     *
     * @return true 沒有鎖定  false 鎖定
     */
    @Override
    public boolean isAccountNonLocked() {
        return appUser.getLockStatus() == null || appUser.getLockStatus() == 0;
    }

    /**
     * todo 判斷信用憑證是否過期,默認沒有過期
     *
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 判斷賬號是否可用,默認可用
     *
     * @return
     */
    @Override
    public boolean isEnabled() {
        return appUser.getDelStatus() == 0;
    }
}

可選:實現PasswordEncoder 接口(密碼加密)

/**
 * PasswordEncoderImpl
 *
 * @author maxzhao
 * @date 2019-05-23 15:55
 */
@Service("passwordEncoder")
public class PasswordEncoderImpl implements PasswordEncoder {
    private final int strength;
    private final SecureRandom random;
    private Pattern BCRYPT_PATTERN;
    private Logger logger;

    /**
     * 構造函數用于設置不同的加密過程
     */
    public PasswordEncoderImpl() {
        this(-1);
    }

    public PasswordEncoderImpl(int strength) {
        this(strength, null);
    }

    public PasswordEncoderImpl(int strength, SecureRandom random) {
        this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2a?\\$\\d\\d\\$[./0-9A-Za-z]{53}");
        this.logger = LoggerFactory.getLogger(this.getClass());
        if (strength == -1 || strength >= 4 && strength <= 31) {
            this.strength = strength;
            this.random = random;
        } else {
            throw new IllegalArgumentException("Bad strength");
        }
    }

    /**
     * 對原始密碼進行編碼。通常,一個好的編碼算法應用SHA-1或更大的哈希值和一個8字節或更大的隨機生成的salt。
     * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or greater hash combined with an 8-byte or greater randomly generated salt.
     *
     * @param rawPassword
     * @return
     */
    @Override
    public String encode(CharSequence rawPassword) {
        String salt;
        if (this.strength > 0) {
            if (this.random != null) {
                salt = BCrypt.gensalt(this.strength, this.random);
            } else {
                salt = BCrypt.gensalt(this.strength);
            }
        } else {
            salt = BCrypt.gensalt();
        }

        return BCrypt.hashpw(rawPassword.toString(), salt);
    }

    /**
     * 驗證從存儲中獲得的已編碼密碼在經過編碼后是否與提交的原始密碼匹配。
     * 如果密碼匹配,返回true;如果密碼不匹配,返回false。存儲的密碼本身永遠不會被解碼。
     *
     * @param rawPassword     the raw password to encode and match
     * @param encodedPassword the encoded password from storage to compare with
     * @return
     */
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (encodedPassword != null && encodedPassword.length() != 0) {
            if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
                this.logger.warn("Encoded password does not look like BCrypt");
                return false;
            } else {
                return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
            }
        } else {
            this.logger.warn("Empty encoded password");
            return false;
        }
    }

    /**
     * 如果為了更好的安全性,應該再次對已編碼的密碼進行編碼,則返回true,否則為false。
     *
     * @param encodedPassword the encoded password to check
     * @return Returns true if the encoded password should be encoded again for better security, else false. The default implementation always returns false.
     */
    @Override
    public boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

驗證用戶登錄信息

/**
 * <p>用戶賬號的驗證</p>
 * <p>JwtAuthenticationFilter</p>
 *
 * @author maxzhao
 * @date 2019-07-04 14:38
 */
@Slf4j
@Component
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private AuthenticationManager authenticationManager;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        //todo 與 WebSecurityConfig 中的 loginProcessingUrl 優先級 有帶判斷
        super.setFilterProcessesUrl("/auth/login");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//        return super.attemptAuthentication(request, response);
        // 從輸入流中獲取到登錄的信息
        try {
            AppUser appUser = new ObjectMapper().readValue(request.getInputStream(), AppUser.class);
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(appUser.getLoginName(), appUser.getPassword(), new ArrayList<>())
            );
        } catch (IOException e) {
            log.error("獲取登錄信息失敗");
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(null, null, new ArrayList<>())
            );
        }
    }

    //
    //

    /**
     * 成功驗證后調用的方法.
     * 如果驗證成功,就生成token并返回
     *
     * @param request
     * @param response
     * @param chain
     * @param authResult
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {

        // 查看源代碼會發現調用getPrincipal()方法會返回一個實現了`UserDetails`接口的對象
        // 所以就是JwtUser啦
        UserDetailsImpl userDetails = (UserDetailsImpl) authResult.getPrincipal();
        String token = JwtTokenUtil.createToken("gtboot", userDetails.getUsername(), 1800L);
        // 返回創建成功的token
        // 但是這里創建的token只是單純的token
        // 按照jwt的規定,最后請求的格式應該是 `Bearer token`
        response.setHeader("token", JwtTokenUtil.TOKEN_PREFIX + token);
        // response.getWriter().write 中文亂碼處理
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(ResultObj.getDefaultResponse(JwtTokenUtil.TOKEN_PREFIX + token, "登錄成功").toJSON());
    }


    /**
     * 這是驗證失敗時候調用的方法
     *
     * @param request
     * @param response
     * @param failed
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        // response.getWriter().write 中文亂碼處理
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(ResultObj.getResponse("登錄失敗", "authentication failed, reason: " + failed.getMessage(), ResultObj.ResponseStatus.LOGIN_FAIL).toJSON());
        log.error(failed.getMessage());
    }
}

用戶權限的攔截器

登錄成功后才會執行此類

/**
 * <p>用戶權限的驗證</p>
 * <p>JwtAuthorizationFilter</p>
 *
 * @author maxzhao
 * @date 2019-07-04 14:39
 */
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

    public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws IOException, ServletException {

        String tokenHeader = request.getHeader(JwtTokenUtil.TOKEN_HEADER);
        // 如果請求頭中沒有Authorization信息則直接放行了
        if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX)) {
            chain.doFilter(request, response);
            return;
        }
        // 如果請求頭中有token,則進行解析,并且設置認證信息
        SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
        super.doFilterInternal(request, response, chain);
    }

    // 這里從token中獲取用戶信息并新建一個token
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
        String token = tokenHeader.replace(JwtTokenUtil.TOKEN_PREFIX, "");
        String username = JwtTokenUtil.getProperties(token);
        if (username != null) {
            return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
        }
        return null;
    }
}

security 配置

/**
 * web
 * EnableGlobalMethodSecurity 啟用方法級的權限認證
 *
 * @author maxzhao
 * @PostMapping
 * @PreAuthorize("hasRole('ADMIN')") public String new(){
 * return "創建";
 * }
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    private final Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);

    @Resource(name = "userDetailsService")
    private UserDetailsService userDetailsService;
    @Resource(name = "passwordEncoder")
    private PasswordEncoder passwordEncoder;
    @Resource(name = "authenticationProvider")
    private AuthenticationProvider authenticationProvider;
//todo springboot + spring security驗證token進行用戶認證  https://blog.csdn.net/menglinjie/article/details/84390503

    /**
     * 自定義用戶認證邏輯
     * 設定用戶訪問權限
     * 用戶身份可以訪問
     * 定義需要攔截的URL
     * 登錄后續操作
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        this.logger.debug("Using start.config.WebSecurityConfig configure(HttpSecurity). ");
        String[] permitAllMatchers = new String[]{"/", "/home", "/js/vue.js", "/auth/register"};
        http
                // 定義哪些URL需要被保護、哪些不需要被保護
                .authorizeRequests()
                // 設置所有人都可以訪問home頁面
                .antMatchers(permitAllMatchers)
                .permitAll()
                // 任何請求,登錄后可以訪問
                .anyRequest()
                // 驗證后可以訪問
                .authenticated()
                .and()
                // 用戶賬號的驗證
                .addFilter(new JwtAuthenticationFilter(authenticationManager()))
                // 用戶權限的驗證
                .addFilter(new JwtAuthorizationFilter(authenticationManager()))
                // 不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 統一異常處理
                .exceptionHandling()
                // 403 異常
                .authenticationEntryPoint(new JwtAuthenticationEntryPoint())
                .and()
                // 定義當需要用戶登錄時候,轉到的登錄頁面。
                .formLogin()
                // 默認/login 在抽象類AbstractAuthenticationFilterConfigurer
                // 用戶沒有登錄時,跳轉到登錄界面,下面的用戶未登錄時,訪問的地址
                // 登錄失敗也跳轉到這里
                .loginPage("/auth/login/fail")
                // 自定義的登錄接口,默認為 '/login'  this.loginPage , 在抽象類AbstractAuthenticationFilterConfigurer
                // 還是走的 security 的接口
                // .loginProcessingUrl("/appLogin/login")
                .loginProcessingUrl("/auth/login")
                // 自定義登錄成功后的頁面
                // .defaultSuccessUrl("/success")
                .defaultSuccessUrl("/auth/login/success")
//                .failureForwardUrl("/auth/login/fail")
                .permitAll()
                // 默認username 在類 UsernamePasswordAuthenticationFilter,FormLoginConfigurer初始化方法也設置了默認值
                .usernameParameter(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY)
                // 默認password 在類 UsernamePasswordAuthenticationFilter,FormLoginConfigurer初始化方法也設置了默認值
                .passwordParameter(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY)

                /* 在抽象類AbstractAuthenticationFilterConfigurer
                   .successForwardUrl("")
                 在抽象類AbstractAuthenticationFilterConfigurer
                        .failureForwardUrl("")
                 failureForwardUrl沒有設置時,this.failureUrl(this.loginPage + "?error"); , 在抽象類AbstractAuthenticationFilterConfigurer
                        .failureUrl("")*/
                .permitAll()
                .and()
                .httpBasic();
        //暫時禁用CSRF,否則無法提交表單  todo https://www.cnblogs.com/xifengxiaoma/p/10020960.html
        http.csrf().disable();
        http.logout()
        ;

    }

    /**
     * 身份驗證
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .authenticationProvider(authenticationProvider)
        ;

    }

    /**
     * 配置攔截資源
     *
     * @param web
     */
    @Override
    public void configure(WebSecurity web) {
        //解決靜態資源被攔截的問題
        web.ignoring()
                .antMatchers("/js/**", "/css/**", "/img/**");
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }
}

登錄認證

/**
 * AuthenticationProviderImpl
 * 自定義認證服務
 *
 * @author maxzhao
 * @date 2019-05-23 15:43
 */
@Slf4j
@Service("authenticationProvider")
public class AuthenticationProviderImpl implements AuthenticationProvider {
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

    /**
     * 驗證帳號是否鎖定\是否禁用\帳號是否到期
     */
    private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
    /**
     * 驗證憑證\密碼是否已過期
     */
    private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
    /**
     * 用戶緩存,默認沒有緩存
     * 此處不做緩存
     */
    private UserCache userCache = new NullUserCache();
    /**
     * principal 通常是用戶名 或者 UseDetails
     * 這里設置控制,默認為 UseDetails
     */
    private boolean forcePrincipalAsString = false;
    @Resource(name = "userDetailsService")
    private UserDetailsService userDetailsService;

    @Resource(name = "passwordEncoder")
    private PasswordEncoder passwordEncoder;
    /**
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 對 supports 方法的二次校驗,為空或不等拋出錯誤
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                () -> messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));
        // 自定義緩存策略
// this.userCache = new GTBootUserCache();

        // Determine username,authentication.getPrincipal()獲取的就是UserDetail
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();
        // 默認情況下從緩存中(UserCache接口實現)取出用戶信息
        boolean cacheWasUsed = true;

        UserDetails userDetail = this.userCache.getUserFromCache(username);
        if (userDetail == null) {
            // 如果從緩存中取不到用戶,則設置cacheWasUsed 為false,供后面使用
            cacheWasUsed = false;
            // retrieveUser是抽象方法,通過子類來實現獲取用戶的信息,以UserDetails接口形式返回,默認的子類為 DaoAuthenticationProvider
            userDetail = userDetailsService.loadUserByUsername(username);
            if (userDetail == null) {
                log.debug("User '" + username + "' not found");
                throw new UsernameNotFoundException("用戶不存在");
            }
        }
        try {// 驗證帳號是否鎖定\是否禁用\帳號是否到期
            preAuthenticationChecks.check(userDetail);
            // 進一步驗證憑證 和 密碼
            additionalAuthenticationChecks(userDetail,
                    (UsernamePasswordAuthenticationToken) authentication);
        } catch (AuthenticationException exception) {
            if (cacheWasUsed) {// 如果是內存用戶,則再次獲取并驗證
                cacheWasUsed = false;
                userDetail = userDetailsService.loadUserByUsername(username);
                preAuthenticationChecks.check(userDetail);
                additionalAuthenticationChecks(userDetail, (UsernamePasswordAuthenticationToken) authentication);
            } else {
                throw exception;
            }
        }
        //驗證憑證是否已過期
        postAuthenticationChecks.check(userDetail);
        //如果沒有緩存則進行緩存,此處的 userCache是 由 NullUserCache 類實現的,名如其義,該類的 putUserInCache 沒做任何事
        //也可以使用緩存 比如 EhCacheBasedUserCache  或者 SpringCacheBasedUserCache
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(userDetail);
        }
        //以下代碼主要是把用戶的信息和之前用戶提交的認證信息重新組合成一個 authentication 實例返回,返回類是 UsernamePasswordAuthenticationToken 類的實例
        Object principalToReturn = userDetail;

        if (forcePrincipalAsString) {
            principalToReturn = userDetail.getUsername();
        }

        return createSuccessAuthentication(principalToReturn, authentication, userDetail);
        /*

        UsernamePasswordAuthenticationToken token
                = (UsernamePasswordAuthenticationToken) authenticate;
        String username = token.getName();
        UserDetails userDetails = null;

        if (username != null) {
            userDetails = userDetailsService.loadUserByUsername(username);
        }

        String password = userDetails.getPassword();
        //與authentication里面的credentials相比較 todo 加密 token 的密碼
        if (!password.equals(token.getCredentials())) {
            throw new UsernameNotFoundException("Invalid username/password,密碼錯誤");
        }
        //TODO 實現 User 緩存
        //授權
        return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());*/
    }

    @Override
    public boolean supports(Class<?> authentication) {
        //返回true后才會執行上面的authenticate方法,這步能確保authentication能正確轉換類型
        return UsernamePasswordAuthenticationToken.class.equals(authentication);
    }

    public UserCache getUserCache() {
        return userCache;
    }

    /**
     * 設置使用的緩存
     * @param userCache
     */
    public void setUserCache(UserCache userCache) {
        this.userCache = userCache;
    }

    /**
     * 驗證帳號是否鎖定\是否禁用\帳號是否到期
     */
    private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
        public void check(UserDetails user) {
            if (!user.isAccountNonLocked()) {
                log.debug("User account is locked");

                throw new LockedException(messages.getMessage(
                        "AuthenticationProviderImpl.locked",
                        "賬號已被鎖定"));
            }
            if (!user.isEnabled()) {
                log.debug("User account is disabled");

                throw new DisabledException(messages.getMessage(
                        "AuthenticationProviderImpl.disabled",
                        "用戶已被禁用"));
            }
            if (!user.isAccountNonExpired()) {
                log.debug("User account is expired");
                throw new AccountExpiredException(messages.getMessage(
                        "AuthenticationProviderImpl.expired",
                        "賬號已過期"));
            }
        }
    }

    /**
     * 驗證憑證是否已過期
     */
    private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
        public void check(UserDetails user) {
            if (!user.isCredentialsNonExpired()) {
                log.debug("User account credentials have expired");

                throw new CredentialsExpiredException(messages.getMessage(
                        "AuthenticationProviderImpl.credentialsExpired",
                        "憑證已過期"));
            }
        }
    }

    /**
     * 驗證密碼
     *
     * @param userDetails
     * @param authentication
     * @throws AuthenticationException
     */
    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            log.debug("Authentication failed: no credentials provided");

            throw new BadCredentialsException(messages.getMessage(
                    "AuthenticationProviderImpl.badCredentials",
                    "無效憑證(無效密碼)"));
        }

        String presentedPassword = authentication.getCredentials().toString();

        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            log.debug("Authentication failed: password does not match stored value");

            throw new BadCredentialsException(messages.getMessage(
                    "AuthenticationProviderImpl.badCredentials",
                    "密碼錯誤"));
        }
    }

    protected Authentication createSuccessAuthentication(Object principal,
                                                         Authentication authentication, UserDetails user) {
        boolean upgradeEncoding = this.userDetailsService != null
                && this.passwordEncoder.upgradeEncoding(user.getPassword());
        String presentedPassword = authentication.getCredentials().toString();
        String newPassword = upgradeEncoding ? this.passwordEncoder.encode(presentedPassword) : presentedPassword;
        return new UsernamePasswordAuthenticationToken(principal, newPassword, user.getAuthorities());
    }
}

我自己小白,看類中寫的注釋,就懂了。

附錄:403 錯誤返回攔截

/**
 * <p>403響應</p>
 * JwtAuthenticationEntryPoint
 *
 * @author maxzhao
 * @date 2019-07-04 18:24
 */
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    /**
     * Commences an authentication scheme.
     * 啟動身份驗證方案。.
     * <p>填充 populate
     * <code>ExceptionTranslationFilter</code> will populate  the <code>HttpSession</code>
     * attribute named
     * <code>AbstractAuthenticationProcessingFilter.SPRING_SECURITY_SAVED_REQUEST_KEY</code>
     * with the requested target URL before calling this method.
     * <p>
     * Implementations should modify the headers on the <code>ServletResponse</code> as
     * necessary to commence the authentication process.
     *
     * @param request       that resulted in an <code>AuthenticationException</code>
     * @param response      so that the user agent can begin authentication
     * @param authException that caused the invocation
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        // 么有權限
        // Full authentication is required to access this resource
        //
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
//        String reason = "統一處理,原因:" + authException.getMessage();
        response.getWriter().write(ResultObj.getErrorResponse("", "統一處理,原因:" + authException.getMessage()).toJSON());
//        response.getWriter().write(new ObjectMapper().writeValueAsString(reason));
    }
}

附錄:token工具類

/**
 * <p>jjwt封裝一下方便調用</p>
 * <p>JwtTokenUtil</p>
 *
 * @author maxzhao
 * @date 2019-07-04 13:30
 */
public class JwtTokenUtil {
    public static final String TOKEN_HEADER = "gtboot";
    public static final String TOKEN_PREFIX = "gtboot ";

    /**
     * 密鑰
     */
    private static final String SECRET = "jwt_secret_gtboot";
    private static final String ISS = "gtboot";

    /**
     * 過期時間是 1800 秒
     */
    private static final long EXPIRATION = 1800L;

    public static String createToken(String issuer, String subject, long expiration) {
        return createToken(issuer, subject, expiration, null);
    }

    /**
     * 創建 token
     *
     * @param issuer     簽發人
     * @param subject    主體,即用戶信息的JSON
     * @param expiration 有效時間(秒)
     * @param claims     自定義參數
     * @return
     * @description todo https://www.cnblogs.com/wangshouchang/p/9551748.html
     */
    public static String createToken(String issuer, String subject, long expiration, Claims claims) {
        return Jwts.builder()
                // JWT_ID:是JWT的唯一標識,根據業務需要,這個可以設置為一個不重復的值,主要用來作為一次性token,從而回避重放攻擊。
//                .setId(id)
                // 簽名算法以及密匙
                .signWith(SignatureAlgorithm.HS512, SECRET)
                // 自定義屬性
                .setClaims(null)
                // 主題:代表這個JWT的主體,即它的所有人,這個是一個json格式的字符串,可以存放什么userid,roldid之類的,作為什么用戶的唯一標志。
                .setSubject(subject)
                // 受眾
//                .setAudience(loginName)
                // 簽發人
                .setIssuer(Optional.ofNullable(issuer).orElse(ISS))
                // 簽發時間
                .setIssuedAt(new Date())
                // 過期時間
                .setExpiration(new Date(System.currentTimeMillis() + (expiration > 0 ? expiration : EXPIRATION) * 1000))
                .compact();
    }

    /**
     * 從 token 中獲取主題信息
     *
     * @param token
     * @return
     */
    public static String getProperties(String token) {
        return getTokenBody(token).getSubject();
    }


    /**
     * 校驗是否過期
     *
     * @param token
     * @return
     */
    public static boolean isExpiration(String token) {
        return getTokenBody(token).getExpiration().before(new Date());
    }

    /**
     * 獲得 token 的 body
     *
     * @param token
     * @return
     */
    private static Claims getTokenBody(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody();
    }
}

本文地址:

SpringBoot+Security+JWT進階:二、自定義認證實踐

推薦
SpringBoot+Security+JWT基礎
SpringBoot+Security+JWT進階:一、自定義認證
SpringBoot+Security+JWT進階:二、自定義認證實踐
gitee多數據源
IDEA好用的插件

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。