SpringSecurity

SpringSecurity

github:https://github.com/Fly0708/securityDemo.git
一、框架的基础使用

当pom文件中加入Spring Security依赖时,默认开启权限认证

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

自定义SecurityConfig配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SuccessHandler successHandler;

    @Autowired
    private FailureHandler failureHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            //basic和表单两种方式,formLogin的拦截器UsernamePasswordAuthenticationFilter
                .formLogin()
                .loginPage("/login")
            //自定义登录url(覆盖AbstractAuthenticationProcessingFilter中的RequestMatcher),post方式,
                .loginProcessingUrl("/login")
            //覆盖默认成功处理器,默认实现跳转上一url,先返回登录用户信息json
                .successHandler(successHandler)
            //覆盖默认失败处理器,默认实现跳转登录页面loginPage,先返回失败信息
                .failureHandler(failureHandler)
                .and()
            //对授权的路径进行配置,
                .authorizeRequests()
                .antMatchers("/login.html","/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .csrf().disable();
    }

    //spring security内置的加密器,加密中的字符串中有salt,每次加密的salt均不一样,所以对同一内容加密后的结果是不同的。
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

SuccessHandler.java

@Component
public class SuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        ObjectMapper objectMapper = new ObjectMapper();

        response.getWriter().write(objectMapper.writeValueAsString(authentication));
    }
}

FailureHandler.java

@Component
public class FailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(500);
        ObjectMapper objectMapper = new ObjectMapper();
        String content = objectMapper.writeValueAsString(exception.getMessage());
        response.getWriter().write(content);
    }
}

UserService.java

//为方便,模拟从数据库查询用户信息
@Service
public class UserService implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Set<GrantedAuthority> authorities = new HashSet<>();

        authorities.add(new SimpleGrantedAuthority("admin"));
        return new User("fly",passwordEncoder.encode("123"),authorities);
    }
}

效果:

访问http://localhost:8080/test,返回{"result":"0","message":"请登录","data":null}

访问http://localhost:8080/login.html,输入错误用户名或密码,返回{"result":"0","message":"用户名或密码错误","data":null}//具体错误信息可通过覆盖DaoAuthenticationProvider或者在UserDetailService中添加逻辑代码实现。输入正确用户名 密码,返回

{
    "authorities": [{
        "authority": "admin"
    }],
    "details": {
        "remoteAddress": "0:0:0:0:0:0:0:1",
        "sessionId": "21385DFFE1DC1DBEE43CDB6F8FD00444"
    },
    "authenticated": true,
    "principal": {
        "password": null,
        "username": "fly",
        "authorities": [{
            "authority": "admin"
        }],
        "accountNonExpired": true,
        "accountNonLocked": true,
        "credentialsNonExpired": true,
        "enabled": true
    },
    "credentials": null,
    "name": "fly"
}

自定义格式在SuccessHandler中实现。

过程分析

一张经典的SpringSecurity执行图

SpringSecurity的执行是依赖一套FilterChain来实现的

SecurityContextPersistenceFilter是第二个Filter,通过断点方式来看这条Filter Chain

1571972343156.png

比较重要的几个Filter:

SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder中,用于FilterSecurityInterceptor判断是否已经认证,SecurityContextHolder中存储。
ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();SecurityContextHolder中的SecurityContext是线程级变量

UsernamePasswordAuthenticationFilter:拦截请求(看其构造函数中配置)。这里采用了模板的方式,具体拦截过程在AbstractAuthenticationProcessingFilter中的doFilter方法。

FilterSecurityInterceptor:根据配置,即那些请求需要permitAll ,哪些请求需要authenticated。

主要的验证逻辑是在UsernamePasswordAuthenticationFilter中实现的

org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#UsernamePasswordAuthenticationFilter(构造函数)


    public UsernamePasswordAuthenticationFilter() {
    //这个类继承了一个抽象类,这里的构造函数设置了父类的requiresAuthenticationRequestMatcher属性(在判断是否拦截请求的时候用到)
        super(new AntPathRequestMatcher("/login", "POST"));
    }

org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#doFilter

        ...
        //用上面的requiresAuthenticationRequestMatcher进行匹配判断,即拦截路径/login POST请求
        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);

            return;
        }
        ...
        
        try {
        //尝试进行验证 子类实现(关键方法)
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                // return immediately as subclass has indicated that it hasn't completed
                // authentication
                return;
            }
            sessionStrategy.onAuthentication(authResult, request, response);
        }
        catch{
        ...
        }
        ...
        //调用登录成功处理器
        successfulAuthentication(request, response, chain, authResult);

org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication

    public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String username = obtainUsername(request);
        String password = obtainPassword(request);

        if (username == null) {
            username = "";
        }

        if (password == null) {
            password = "";
        }

        username = username.trim();

        //封装用于验证的token
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);

        // Allow subclasses to set the "details" property sessionID,封装请求的一些信息
        setDetails(request, authRequest);

        //得到AuthenticationManager对象(全局唯一)的实现类ProviderManager进行登录验证
        return this.getAuthenticationManager().authenticate(authRequest);
    }

​ org.springframework.security.authentication.ProviderManager#authenticate

...
//通过循环所注册的provider,通过provider的support(token)方法查找能处理该类型token的provider
for (AuthenticationProvider provider : getProviders()) {
    if (!provider.supports(toTest)) {
                continue;
            }
}
try {
            //支持UsernamePasswordAuthenticationToken的是DaoAuthenticationProvider
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }

org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate

preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user),

org.springframework.security.authentication.dao.DaoAuthenticationProvider#retrieveUser

//利用userDetailService查找用户,security自动配置
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;

org.springframework.security.authentication.dao.DaoAuthenticationProvider#additionalAuthenticationChecks

...
//这个方法对密码进行了校验
    String presentedPassword = authentication.getCredentials().toString();

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

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }

整体的流程是这样的,

一张经典的图

1571974749801.png

所以可以通过添加自定义filter provider token可以实现自定义登录流程,

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