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好用的插件

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345