SpringBoot+Security+JWT进阶:一、自定义认证

title: SpringBoot+Security+JWT进阶:一、自定义认证
date: 2019-07-04
author: maxzhao
tags:
- JAVA
- SpringBoot
- Security
- JWT
- Authentication
categories:
- SpringBoot
- Security+JWT

这里谈谈自定义认证

不使用自定义认证的 WebSecurityConfig

@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;
//**********************
// 略
//**********************

    /**
     * 身份验证
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder)
        ;

    }
}

不使用自定义认证的验证类 AbstractUserDetailsAuthenticationProvider

这里只看 authenticate验证的方法,根据自己的理解,我写上了注释,//// 为重点强调

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
    // 对 supports 方法的二次校验,为空或不等抛出错误
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                () -> messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));

        // Determine username,authentication.getPrincipal()获取的就是UserDetail
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();
        // 默认情况下从缓存中(UserCache接口实现)取出用户信息
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            // 如果从缓存中取不到用户,则设置cacheWasUsed 为false,供后面使用
            cacheWasUsed = false;
            try {
                // retrieveUser是抽象方法,通过子类来实现获取用户的信息,以UserDetails接口形式返回,默认的子类为 DaoAuthenticationProvider
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            //// 这就是为什么 UsernameNotFoundException 抛出信息却获取不到的原因
            catch (UsernameNotFoundException notFound) {
                logger.debug("User '" + username + "' not found");
                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));
                }
                else {
                    throw notFound;
                }
            }
            Assert.notNull(user,
                    "retrieveUser returned null - a violation of the interface contract");
        }
        try {// 验证帐号是否锁定\是否禁用\帐号是否到期
            preAuthenticationChecks.check(user);
            // 进一步验证凭证 和 密码
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException exception) {
            if (cacheWasUsed) {// 如果是内存用户,则再次获取并验证
                // There was a problem, so try again after checking
                // we're using latest data (i.e. not from the cache)
                cacheWasUsed = false;
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            else {
                throw exception;
            }
        }
        //验证凭证是否已过期
        postAuthenticationChecks.check(userDetail);
    //如果没有缓存则进行缓存,此处的 userCache是 由 NullUserCache 类实现的,名如其义,该类的 putUserInCache 没做任何事
        //也可以使用缓存 比如 EhCacheBasedUserCache  或者 SpringCacheBasedUserCache
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }
        //以下代码主要是把用户的信息和之前用户提交的认证信息重新组合成一个 authentication 实例返回,返回类是 UsernamePasswordAuthenticationToken 类的实例
        Object principalToReturn = user;

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

        return createSuccessAuthentication(principalToReturn, authentication, user);
    }
@startuml
Title "Authentication类图"
interface Principal
interface Authentication
interface AuthenticationManager
interface AuthenticationProvider
abstract class AbstractUserDetailsAuthenticationProvider
class ProviderManager
class DaoAuthenticationProvider
interface UserDetailsService


Principal <|-- Authentication
Authentication <.. AuthenticationManager
AuthenticationManager <|-- ProviderManager
ProviderManager  o--> AuthenticationProvider
AuthenticationProvider <|.. AbstractUserDetailsAuthenticationProvider
AbstractUserDetailsAuthenticationProvider <|-- DaoAuthenticationProvider
UserDetailsService <.. AbstractUserDetailsAuthenticationProvider

interface AuthenticationManager{
# Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
}
abstract class AbstractUserDetailsAuthenticationProvider{
+ public Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
+public boolean supports(Class<?> authentication);
}
@enduml

使用自定义认证的 WebSecurityConfig

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    private final Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);
    
    private PasswordEncoder passwordEncoder;
    @Resource(name = "authenticationProvider")
    private AuthenticationProvider authenticationProvider;
//**********************
// 略
//**********************

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

    }
}

自定义认证类 AuthenticationProviderImpl

/**
 * AuthenticationProviderImpl
 * 自定义认证服务
 *
 * @author maxzhao
 * @date 2019-05-23 15:43
 */
@Service("authenticationProvider")
public class AuthenticationProviderImpl implements AuthenticationProvider {
    @Resource(name = "userDetailsService")
    private UserDetailsService userDetailsService;

    @Resource(name = "passwordEncoder")
    private PasswordEncoder passwordEncoder;

    /**
     *
     * @param authenticate
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authenticate) throws AuthenticationException {
        UsernamePasswordAuthenticationToken token
                = (UsernamePasswordAuthenticationToken) authenticate;
        String username = token.getName();
        UserDetails userDetails = null;

        if (username != null) {
            userDetails = userDetailsService.loadUserByUsername(username);
        }
        if (userDetails == null) {
            throw new UsernameNotFoundException("用户名/密码无效");
        } else if (!userDetails.isEnabled()) {
            System.out.println("jinyong用户已被禁用");
            throw new DisabledException("用户已被禁用");
        } else if (!userDetails.isAccountNonExpired()) {
            System.out.println("guoqi账号已过期");
            throw new AccountExpiredException("账号已过期");
        } else if (!userDetails.isAccountNonLocked()) {
            System.out.println("suoding账号已被锁定");
            throw new LockedException("账号已被锁定");
        } else if (!userDetails.isCredentialsNonExpired()) {
            System.out.println("pingzheng凭证已过期");
            throw new CredentialsExpiredException("凭证已过期");
        }
        String password = userDetails.getPassword();
        //与authentication里面的credentials相比较 ; todo 加密 token 中的密码
        if (!password.equals(token.getCredentials())) {
            throw new BadCredentialsException("Invalid username/password");
        }
        //授权
        return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        //返回true后才会执行上面的authenticate方法,这步能确保authentication能正确转换类型
        return UsernamePasswordAuthenticationToken.class.equals(authentication);
    }
}

本文地址:
SpringBoot+Security+JWT进阶:一、自定义认证

推荐

SpringBoot+Security+JWT基础
SpringBoot+Security+JWT进阶:一、自定义认证
SpringBoot+Security+JWT进阶:二、自定义认证实践

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