spring security 原理+實戰


瘋狂創客圈 Java 高並發【 億級流量聊天室實戰】實戰系列 【博客園總入口

架構師成長+面試必備之 高並發基礎書籍 【Netty Zookeeper Redis 高並發實戰


前言

Crazy-SpringCloud 微服務腳手架 &視頻介紹

Crazy-SpringCloud 微服務腳手架,是為 Java 微服務開發 入門者 准備的 學習和開發腳手架。並配有一系列的使用教程和視頻,大致如下:

高並發 環境搭建 圖文教程和演示視頻,陸續上線:

中間件 鏈接地址
Linux Redis 安裝(帶視頻) Linux Redis 安裝(帶視頻)
Linux Zookeeper 安裝(帶視頻) Linux Zookeeper 安裝, 帶視頻
Windows Redis 安裝(帶視頻) Windows Redis 安裝(帶視頻)
RabbitMQ 離線安裝(帶視頻) RabbitMQ 離線安裝(帶視頻)
ElasticSearch 安裝, 帶視頻 ElasticSearch 安裝, 帶視頻
Nacos 安裝(帶視頻) Nacos 安裝(帶視頻)

Crazy-SpringCloud 微服務腳手架 圖文教程和演示視頻,陸續上線:

組件 鏈接地址
Eureka Eureka 入門,帶視頻
SpringCloud Config springcloud Config 入門,帶視頻
spring security spring security 原理+實戰
Spring Session SpringSession 獨立使用
分布式 session 基礎 RedisSession (自定義)
重點: springcloud 開發腳手架 springcloud 開發腳手架
SpingSecurity + SpringSession 死磕 (寫作中) SpingSecurity + SpringSession 死磕

小視頻以及所需工具的百度網盤鏈接,請參見 瘋狂創客圈 高並發社群 博客

Spring Security 的重要性

在web應用開發中,安全無疑是十分重要的,選擇Spring Security來保護web應用是一個非常好的選擇。Spring Security 是spring項目之中的一個安全模塊,特別是在spring boot項目中,spring security已經默認集成和啟動了。

Spring Security 默認為自動開啟的,可見其重要性。

如果要關閉,需要在啟動類加上,exclude ={SecurityAutoConfiguration} 的配置

@EnableEurekaClient
@SpringBootApplication(scanBasePackages = {
        "com.crazymaker.springcloud.user",
        "com.crazymaker.springcloud.seckill.remote.fallback",
        "com.crazymaker.springcloud.standard"
}, exclude = {SecurityAutoConfiguration.class})

或者

spring:
  autoconfigure:
    exclude: org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration

一般不建議關閉。

Spring Security 核心組件

spring security核心組件有:Userdetails 、Authentication,UserDetailsService、AuthenticationProvider、AuthenticationManager 下面分別介紹。

Authentication

authentication 直譯過來是“認證”的意思,在Spring Security 中Authentication用來表示當前用戶是誰,一般來講你可以理解為authentication就是一組用戶名密碼信息。Authentication也是一個接口,其定義如下:

public interface Authentication extends Principal, Serializable {

Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

接口有4個get方法,分別獲取

  • Authorities, 填充的是用戶角色信息。

  • Credentials,直譯,證書。填充的是密碼。

  • Details ,用戶信息。

  • Principal 直譯,形容詞是“主要的,最重要的”,名詞是“負責人,資本,本金”。感覺很別扭,所以,還是不翻譯了,直接用原詞principal來表示這個概念,其填充的是用戶名。
    因此可以推斷其實現類有這4個屬性。

這幾個方法作用如下:

  • getAuthorities: 獲取用戶權限,一般情況下獲取到的是用戶的角色信息。

  • getCredentials: 獲取證明用戶認證的信息,通常情況下獲取到的是密碼等信息。

  • getDetails: 獲取用戶的額外信息,(這部分信息可以是我們的用戶表中的信息)

  • getPrincipal: 獲取用戶身份信息,在未認證的情況下獲取到的是用戶名,在已認證的情況下獲取到的是 UserDetails (UserDetails也是一個接口,里邊的方法有getUsername,getPassword等)。

  • isAuthenticated: 獲取當前 Authentication 是否已認證。

  • setAuthenticated: 設置當前 Authentication 是否已認證(true or false)。

UserDetails

UserDetails,看命知義,是用戶信息的意思。其存儲的就是用戶信息,其定義如下:

public interface UserDetails extends Serializable {

Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}

方法含義如下:

  • getAuthorites:獲取用戶權限,本質上是用戶的角色信息。

  • getPassword: 獲取密碼。

  • getUserName: 獲取用戶名。

  • isAccountNonExpired: 賬戶是否過期。

  • isAccountNonLocked: 賬戶是否被鎖定。

  • isCredentialsNonExpired: 密碼是否過期。

  • isEnabled: 賬戶是否可用。

UserDetailsService

提到了UserDetails就必須得提到UserDetailsService, UserDetailsService也是一個接口,且只有一個方法loadUserByUsername,他可以用來獲取UserDetails。


package org.springframework.security.core.userdetails;

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

通常在spring security應用中,我們會自定義一個CustomUserDetailsService來實現UserDetailsService接口,並實現其public UserDetails loadUserByUsername(final String login);方法。我們在實現loadUserByUsername方法的時候,就可以通過查詢數據庫(或者是緩存、或者是其他的存儲形式)來獲取用戶信息,然后組裝成一個UserDetails,(通常是一個org.springframework.security.core.userdetails.User,它繼承自UserDetails) 並返回。

在實現loadUserByUsername方法的時候,如果我們通過查庫沒有查到相關記錄,需要拋出一個異常來告訴spring security來“善后”。這個異常是org.springframework.security.core.userdetails.UsernameNotFoundException。

AuthenticationProvider

負責真正的驗證。

當我們使用 authentication-provider 元素來定義一個 AuthenticationProvider 時,如果沒有指定對應關聯的 AuthenticationProvider 對象,Spring Security 默認會使用 DaoAuthenticationProvider。DaoAuthenticationProvider 在進行認證的時候需要一個 UserDetailsService 來獲取用戶的信息 UserDetails,其中包括用戶名、密碼和所擁有的權限等。所以如果我們需要改變認證的方式,我們可以實現自己的 AuthenticationProvider;如果需要改變認證的用戶信息來源,我們可以實現 UserDetailsService。

實現了自己的 AuthenticationProvider 之后,我們可以在配置文件中這樣配置來使用我們自己的 AuthenticationProvider。其中 myAuthenticationProvider 就是我們自己的 AuthenticationProvider 實現類對應的 bean。

AuthenticationProvider 接口如下:

package org.springframework.security.authentication;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

public interface AuthenticationProvider {
    Authentication authenticate(Authentication var1) throws AuthenticationException;

    boolean supports(Class<?> var1);
}

  • authenticate 表示認證的動作。

  • supports 表示所支持的 Authentication類型。Authentication 包含很多子類,如果 AbstractAuthenticationToken 。

AbstractAuthenticationToken implements Authentication

還有,可以自定義 Authentication ,比如 本實例所使用的: JwtAuthenticationToken。

AuthenticationManager

認證是由 AuthenticationManager 來管理的,但是真正進行認證的是 AuthenticationManager 中定義的 AuthenticationProvider。AuthenticationManager 中可以定義有多個 AuthenticationProvider。

AuthenticationManager 是一個接口,它只有一個方法,接收參數為Authentication,其定義如下:

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
}

AuthenticationManager 的作用就是校驗Authentication,如果驗證失敗會拋出AuthenticationException異常。AuthenticationException是一個抽象類,因此代碼邏輯並不能實例化一個AuthenticationException異常並拋出,實際上拋出的異常通常是其實現類,如DisabledException,LockedException,BadCredentialsException等。BadCredentialsException可能會比較常見,即密碼錯誤的時候。

組件比較多,但是如果主要流程理順了,也比較簡單。

Spring Security 實戰

搞定兩個 AuthenticationProvider:

(1) 從數據庫獲取用戶

首先通過 UserDetailsService 獲取 UserDetails,然后 通過 UserDetailsService 裝配 DaoAuthenticationProvider

(2) 完成用戶的認證

實現一個自己的 JwtAuthenticationProvider,完成用戶的認證

(3)定制一個過濾器

(4)完成所有組件的裝配

實戰1 : UserDetailsService 獲取 UserDetails

首先通過 UserDetailsService 獲取 UserDetails,然后 通過 UserDetailsService 裝配 DaoAuthenticationProvider。

package com.crazymaker.springcloud.user.info.service.impl;

@Slf4j
@Service
public class UserAuthService implements UserDetailsService {

    private PasswordEncoder passwordEncoder;

    public UserAuthService() {
        //默認使用 bcrypt, strength=10
        this.passwordEncoder =
                PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }


    private UserPO loadFromDB(String username) {
        if (null == userDao)

        {
            userDao = CustomAppContext.getBean(UserDao.class);
        }

        List<UserPO> list = userDao.findAllByLoginName(username);

        if (null == list || list.size() <= 0) {
            return null;
        }
        UserPO userPO = list.get(0);
        return userPO;
    }

 

    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {


        UserPO userPO = loadFromDB(username);


        //將salt放到password字段返回
        return User.builder()
                .username(userPO.getLoginName())
                .password(userPO.getPassword())
//                .password(SessionConstants.SALT)
                //BCrypt.gensalt();  正式開發時可以調用該方法實時生成加密的salt
//                .password(SessionConstants.SALT)
                .authorities(SessionConstants.USER_INFO)
                .roles("USER")
                .build();

    }


}

實戰2: 裝配 DaoAuthenticationProvider

在 SecurityConfiguration 配置類中加入如下內容:


    @Bean("daoAuthenticationProvider")
    protected AuthenticationProvider daoAuthenticationProvider() throws Exception {
        //這里會默認使用BCryptPasswordEncoder比對加密后的密碼,注意要跟createUser時保持一致
        DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider();
        daoProvider.setUserDetailsService(userDetailsService());
        return daoProvider;
    }


    @Override
    protected UserDetailsService userDetailsService() {
        return new UserAuthService();
    }

實戰3: 實現一個自己的 JwtAuthenticationProvider

繼承於 AuthenticationProvider,實現一個自己的 JwtAuthenticationProvider,完成用戶的認證

package com.crazymaker.springcloud.standard.security.provider;
//...

public class JwtAuthenticationProvider implements AuthenticationProvider {

    private RedisOperationsSessionRepository sessionRepository;
    private CustomedSessionIdResolver httpSessionIdResolver;

    public JwtAuthenticationProvider(RedisOperationsSessionRepository sessionRepository,
                                     CustomedSessionIdResolver httpSessionIdResolver) {
        this.sessionRepository = sessionRepository;
        this.httpSessionIdResolver = httpSessionIdResolver;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        DecodedJWT jwt = ((JwtAuthenticationToken) authentication).getToken();
        if (jwt.getExpiresAt().before(Calendar.getInstance().getTime())) {
            throw new NonceExpiredException("認證過期");
        }
        String sid = jwt.getSubject();
        String otoken = jwt.getToken();


        Session session = null;

        try {
            session = sessionRepository.findById(sid);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (null == session) {
            throw new NonceExpiredException("認證有誤,請重新登錄");
        }

        String json = session.getAttribute(G_USER);
        if (StringUtils.isBlank(json)) {
            throw new NonceExpiredException("認證有誤,請重新登錄");
        }

        UserDTO userDTO = JsonUtil.jsonToPojo(json, UserDTO.class);
        if (null == userDTO) {
            throw new NonceExpiredException("認證有誤");
        }


        String password = userDTO.getPassword();

        String username = userDTO.getLoginName();
        UserDetails user = User.builder()
                .username(username)
                .password(password)
                .authorities(SessionConstants.USER_INFO)
                .build();

        String encryptSalt = password;
        try {
            Algorithm algorithm = Algorithm.HMAC256(encryptSalt);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withSubject(sid).build();
            verifier.verify(jwt.getToken());
        } catch (Exception e) {
            throw new BadCredentialsException("JWT token verify fail", e);
        }
        JwtAuthenticationToken token =
                new JwtAuthenticationToken(user, jwt, user.getAuthorities());
        return token;
    }


    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.isAssignableFrom(JwtAuthenticationToken.class);
    }

}

實戰4: 裝配 AuthenticationManager

認證是由 AuthenticationManager 來管理的,但是真正進行認證的是 AuthenticationManager 中定義的 AuthenticationProvider。AuthenticationManager 中可以定義有多個 AuthenticationProvider。

  @EnableWebSecurity()
public class UserWebSecurityConfig extends WebSecurityConfigurerAdapter {
  
  @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(daoAuthenticationProvider())
                .authenticationProvider(jwtAuthenticationProvider());
    }
    //....

}

實戰5: 定制過濾器,將 AuthenticationManager 用起來

搞得再多,如果不通過過濾器,將 AuthenticationManager 用起來,也是沒有用的。

package com.crazymaker.springcloud.standard.security.filter;
//.....

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private RequestMatcher requiresAuthenticationRequestMatcher;
    private List<RequestMatcher> permissiveRequestMatchers;
    private AuthenticationManager authenticationManager;


    private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
    private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

     //.....
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        Authentication authResult = null;
        /**
         * 場景: 從 zuul 過來,直接帶上session 頭
         */
        if (StringUtils.isNotEmpty(request.getHeader(SessionConstants.SESSION_SEED))) {
            request.setAttribute(SessionConstants.SESSION_SEED,
                    request.getHeader(SessionConstants.SESSION_SEED));
            UserDetails userDetails = User.builder()
                    .username(request.getHeader(SessionConstants.SESSION_SEED))
                    .password(request.getHeader(SessionConstants.SESSION_SEED))
                    .authorities(SessionConstants.USER_INFO)
                    .build();
            authResult = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
            successfulAuthentication(request, response, filterChain, authResult);

            filterChain.doFilter(request, response);
            return;
        }

        /**
         * 正常場景: 單體微服務訪問,或者從Zuul過來,沒有帶 session head
         */
        if (!requiresAuthentication(request, response)) {

            filterChain.doFilter(request, response);
            return;
        }
        AuthenticationException failed = null;
        try {
            String token = getJwtToken(request);
            if (StringUtils.isNotBlank(token)) {
                JwtAuthenticationToken authToken = new JwtAuthenticationToken(JWT.decode(token));
                DecodedJWT jwt = authToken.getToken();
                
                //將  AuthenticationManager 用起來
                authResult = this.getAuthenticationManager().authenticate(authToken);
                UserDetails user = (UserDetails) authResult.getPrincipal();
                request.setAttribute(SessionConstants.SESSION_SEED, jwt.getSubject());
            } else {
                failed = new InsufficientAuthenticationException("請求頭認證消息為空");
            }
        } catch (JWTDecodeException e) {
            logger.error("JWT format error", e);
            failed = new InsufficientAuthenticationException("請求頭認證消息格式錯誤", failed);
        } catch (InternalAuthenticationServiceException e) {
            logger.error(
                    "An internal error occurred while trying to authenticate the user.",
                    failed);
            failed = e;
        } catch (AuthenticationException e) {
            // Authentication failed
            failed = e;
        }
        if (authResult != null) {
            successfulAuthentication(request, response, filterChain, authResult);
        } else if (!permissiveRequest(request)) {
            unsuccessfulAuthentication(request, response, failed);
            return;
        }

        filterChain.doFilter(request, response);
    }

    protected void unsuccessfulAuthentication(HttpServletRequest request,
                                              HttpServletResponse response, AuthenticationException failed)
            throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        failureHandler.onAuthenticationFailure(request, response, failed);
    }

 //....

}

實戰6: 配置 HttpSecurity 的過濾機制

還是在 UserWebSecurityConfig 配置文件,將 HttpSecurity 的過濾機制配置起來,完成所有組件的裝配。

代碼如下:

package com.crazymaker.springcloud.user.info.config;
//...

@EnableWebSecurity()
public class UserWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserAuthService userAuthService;


    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers(
                        "/v2/api-docs",
                        "/swagger-resources/configuration/ui",
                        "/swagger-resources",
                        "/swagger-resources/configuration/security",
                        "/swagger-ui.html",
                        "/api/user/login/v1",
//                        "/api/user/add/v1",
//                        "/api/user/speed/test/v1",
//                        "/api/user/say/hello/v1",
//                        "/api/user/*/detail/v1",
                        "/api/crazymaker/duty/info/user/login")
                .permitAll()
                .anyRequest().authenticated()

//                .antMatchers("/image/**").permitAll()
//                .antMatchers("/admin/**").hasAnyRole("ADMIN")
                .and()

                .formLogin().disable()
                .sessionManagement().disable()
                .cors()
                .and()

                .addFilterAfter(new OptionsRequestFilter(), CorsFilter.class)
                .apply(new JsonLoginConfigurer<>()).loginSuccessHandler(jsonLoginSuccessHandler())
                .and()
                .apply(new JwtAuthConfigurer<>()).tokenValidSuccessHandler(jwtRefreshSuccessHandler()).permissiveRequestUrls("/logout")
                .and()
                .logout()
//		        .logoutUrl("/logout")   //默認就是"/logout"
                .addLogoutHandler(tokenClearLogoutHandler())
                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
                .and()
                .addFilterBefore(springSessionRepositoryFilter(), SessionManagementFilter.class)
                .sessionManagement().disable()
        ;


    }


    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(
                "/api/user/login/v1",
                "/v2/api-docs",
                "/swagger-resources/configuration/ui",
                "/swagger-resources",
                "/swagger-resources/configuration/security",
//                "/api/user/say/hello/v1",
//                "/api/user/add/v1",
//                "/api/user/speed/test/v1",
//                "/api/user/*/detail/v1",
                "/images/**",
                "/swagger-ui.html",
                "/webjars/**",
                "**/favicon.ico",
                "/css/**",
                "/js/**",
                "/api/crazymaker/info/user/login"
        );

    }


    @Resource
    RedisOperationsSessionRepository sessionRepository;

    @Resource
    public CustomedSessionIdResolver httpSessionIdResolver;

    @DependsOn({"sessionRepository", "httpSessionIdResolver"})
    @Bean("jwtAuthenticationProvider")
    protected AuthenticationProvider jwtAuthenticationProvider() {
        return new JwtAuthenticationProvider(sessionRepository, httpSessionIdResolver);
    }


    public <S extends Session> OncePerRequestFilter springSessionRepositoryFilter() {

        CustomedSessionRepositoryFilter<? extends Session> sessionRepositoryFilter = new CustomedSessionRepositoryFilter<>(
                sessionRepository);
//        sessionRepositoryFilter.setServletContext(this.servletContext);
        sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
        return sessionRepositoryFilter;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(daoAuthenticationProvider())
                .authenticationProvider(jwtAuthenticationProvider());
    }


    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    @Bean("daoAuthenticationProvider")
    protected AuthenticationProvider daoAuthenticationProvider() throws Exception {
        //這里會默認使用BCryptPasswordEncoder比對加密后的密碼,注意要跟createUser時保持一致
        DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider();
        daoProvider.setUserDetailsService(userDetailsService());
        return daoProvider;
    }


    @Bean
    protected JwtRefreshSuccessHandler jwtRefreshSuccessHandler() {
        return new JwtRefreshSuccessHandler();
    }


    @Override
    protected UserDetailsService userDetailsService() {
        return new UserAuthService();
    }


    @Bean
    protected JsonLoginSuccessHandler jsonLoginSuccessHandler() {
        return new JsonLoginSuccessHandler(userAuthService);
    }


    @Bean
    protected TokenClearLogoutHandler tokenClearLogoutHandler() {
        return new TokenClearLogoutHandler(userAuthService);
    }

    @Bean
    protected CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "HEAD", "OPTION"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.addExposedHeader(SessionConstants.AUTHORIZATION);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

}

實戰小結

大概通過以上6步,一個集成jwt的springsecurity機制,完整的配置起來了。

具體,請關注 Java 高並發研習社群博客園 總入口


最后,介紹一下瘋狂創客圈:瘋狂創客圈,一個Java 高並發研習社群博客園 總入口

瘋狂創客圈,傾力推出:面試必備 + 面試必備 + 面試必備 的基礎原理+實戰 書籍 《Netty Zookeeper Redis 高並發實戰

img


瘋狂創客圈 Java 死磕系列

  • Java (Netty) 聊天程序【 億級流量】實戰 開源項目實戰

Java 面試題 一網打盡**



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM