Spring Security -- 安全

概念
認證:即登錄,authentication
授權:即允許某種操作,authorization
會話:即保持已登錄狀態
RBAC:Role-Based Access Control ,基于角色的訪問控制
業務系統:即前臺用戶系統
內管系統:即后臺管理系統

Spring Boot Security 應用組成

1、初始化Spring Boot 應用
2、在pom中增加依賴管理

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.spring.platform</groupId>
                <artifactId>platform-bom</artifactId> <!-- Spring Framework依賴管理,來自Spring IO Platform項目 -->
                <version>Cairo-SR6</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId> <!-- Spring Cloud 依賴管理 -->
                <version>Greenwich.M3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

加了依賴管理后,設置依賴不需要指明版本,且不需要以下配置:

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

3、配置Maven依賴

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

4、從數據庫中查詢用戶詳情的實現類

@Component
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根據用戶名,從數據庫找出用戶信息
        // 參數依次為:用戶名,數據庫里記錄的密碼,可用,未過期,密碼未過期,未被鎖定,權限列表
        // Spring Security 會 自動調用 PasswordEncoder.match() 來判斷密碼是否正確
        return new org.springframework.security.core.userdetails.User(username, passwordEncoder.encode("密碼"), true, true,
                true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_角色1,權限1"));
    }
}

5、登錄成功 、 登錄失敗、登出成功、賬號被踢出、無訪問權限 處理類

@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        // 返回一個 json
        response.getWriter().write(objectMapper.writeValueAsString(authentication)); // authentication 里有權限列表
    }
}
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        // 返回一個 json
        response.getWriter().write(objectMapper.writeValueAsString(e)); // e 認證失敗的原因
    }
}
@Component
public class LogOutHandler implements LogoutSuccessHandler {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        // 返回一個 json
        response.getWriter().write(objectMapper.writeValueAsString(authentication));
    }
}
@Component
public class ExpiredSessionStrategy  implements SessionInformationExpiredStrategy {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        event.getResponse().setContentType("application/json;charset=UTF-8");
        RestfulResult result = new RestfulResult(-1, "您的賬號已在別處登錄");
        event.getResponse().getWriter().write(objectMapper.writeValueAsString(result));
    }
}
@Component
public class AuthenticationEntryPoint implements org.springframework.security.web.AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
        response.setStatus(HttpStatus.FOUND.value());
        response.setHeader("location", "https://**.com/web/sign_out");
    }
}

6、Spring Security 配置類

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private LoginSuccessHandler loginSuccessHandler;
    @Autowired
    private LoginFailureHandler loginFailureHandler;
    @Autowired
    private LogOutHandler logOutHandler;
    @Autowired
    private ExpiredSessionStrategy expiredSessionStrategy;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()      // 使用表單登錄
                .loginPage("/login.html")  // 未登錄時重定向到登錄頁面, 不指定則使用Spring security 默認提供的登錄頁面
                .loginProcessingUrl("/api/login")  // 指定 登錄接口 url
                .successHandler(loginSuccessHandler)  // 指定登錄成功處理類,不指定則重定向
                .failureHandler(loginFailureHandler) // 指定登錄失敗處理類,不指定則重定向
                .and()
                .authorizeRequests()   // 開始授權配置
                .antMatchers("/*.html").permitAll()  // 對*.html 的請求,無需權限
                .antMatchers(HttpMethod.POST, "/manage/*").hasRole("manager") // 對 /manage/* 的請求,需要擁有manager角色
                .antMatchers("/client/*").hasAuthority("client") // 對 /client/* 的請求,需要擁有client權限
                .anyRequest().authenticated()           // 針對所有請求,進行身份認證
                     
                .and()
                .logout()   // 開始 登出配置
                .logoutUrl("/signOut")  // 登出接口,默認為 /logout
                .logoutSuccessUrl("/login.html")  // 登出重定向到的路徑,默認為loginPage
                .logoutSuccessHandler(logOutHandler) // 與logoutSuccessUrl互斥
                .deleteCookies("JSESSION") // 登出時 清理 cookie
                .and()
                .csrf()   // 開始csrf配置
                .disable() // 放開csrf防御
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);    // 無權限訪問的處理
                // 只配置 loginPage 時,可能出現 “無訪問權限時 瀏覽器彈出一個默認登錄框”,為規避這種情況,可以配置authenticationEntryPoint()

                http.sessionManagement().maximumSessions(1)  // 同一賬號只允許一處登錄
                .maxSessionsPreventsLogin(false)    // 允許后登錄者踢出先登錄者
                .expiredSessionStrategy(expiredSessionStrategy);  // 被踢出時的請求響應
        super.configure(http);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 這是Spring提供的一個密碼加密器,加鹽散列,并將鹽拼入散列值,可用防止散列撞庫
        return new BCryptPasswordEncoder(); // 也可以自己實現一個 PasswordEncoder
    }

    @Bean
    // 允許跨域配置
    public CorsConfigurationSource corsConfigurationSource() {
        final CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin(CorsConfiguration.ALL);  // 或 config.setAllowedOriginPatterns(Collections.singletonList("*"));
        config.addAllowedHeader("*");
        config.addAllowedMethod("OPTIONS");  // AllowedMethod 必須羅列,而不能用通配符 *
        config.addAllowedMethod("HEAD");
        config.addAllowedMethod("GET");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("DELETE");
        config.addAllowedMethod("PATCH");
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

圖片驗證碼驗證

1、配置Maven依賴

        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
        </dependency>

2、認證異常類

// AuthenticationException 是抽象類,不能實例化,因此需要自定義一個 驗證碼異常類
public class ValidateCodeException extends AuthenticationException {
    public ValidateCodeException(String msg) {
        super(msg);
    }
}

3、驗證碼過濾器

@Component
public class ValidateCodeFilter extends OncePerRequestFilter {

    @Autowired
    private LoginFailureHandler loginFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 只過濾登錄接口
        if (StringUtils.equals("/api/login", request.getRequestURI()) && StringUtils.equalsIgnoreCase("post", request.getMethod())) {
            try {
                validate(new ServletWebRequest(request));
            }
            catch (ValidateCodeException e) {
                loginFailureHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }
        filterChain.doFilter(request, response);
    }
    private void validate(ServletWebRequest request) throws ServletRequestBindingException {
        // 這里從session中取出驗證碼值進行比對
        throw new ValidateCodeException("驗證碼不匹配");
    }
}

4、Spring Security配置類

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private ValidateCodeFilter validateCodeFilter;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 在驗證賬號 之前 驗證 圖片驗證碼
        http.addFilterBefore( validateCodeFilter, UsernamePasswordAuthenticationFilter.class )
                .formLogin();
        super.configure(http);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 這是Spring提供的一個密碼加密器,加鹽散列,并將鹽拼入散列值,可用防止散列撞庫
        return new BCryptPasswordEncoder(); // 也可以自己實現一個 PasswordEncoder
    }
}

短信登錄

1、仿照 UsernamePasswordAuthenticationToken,定義Token類

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final Object principal;

    public SmsCodeAuthenticationToken(String mobile) {
        super(null);
        this.principal = mobile;
        setAuthenticated(false);
    }

    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    }


    public Object getCredentials() {
        return null;
    }

    public Object getPrincipal() {
        return this.principal;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }
        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}

2、仿照 UsernamePasswordAuthenticationFilter,定義Filter類

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    private String mobileParameter = "mobile";    // 字段名
    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter() {
        super(new AntPathRequestMatcher("/api/mobileLogin", "POST"));  // 短信登錄接口
    }

    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 mobile = obtainMobile(request);
        if (mobile == null) {
            mobile = "";
        }
        mobile = mobile.trim();
        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }


    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }


    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

    public void setMobileParameter(String parameter) {
        this.mobileParameter = parameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getMobileParameter() {
        return mobileParameter;
    }
}

3、仿照 DaoAuthenticationProvider 實現 Provider類

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
        
        UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());

        if (user == null) {
            throw new InternalAuthenticationServiceException("無法獲取用戶信息");
        }
        
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
        
        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }

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

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}

4、短信驗證碼過濾器

// 與圖片驗證碼過濾器 類似

5、配置 Filter類 和 Provider類

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    
    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;
    
    @Autowired
    private AuthenticationFailureHandler imoocAuthenticationFailureHandler;
    
    @Autowired
    private MyUserDetailsService userDetailsService;
    
    @Override
    public void configure(HttpSecurity http) throws Exception {
        
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
        
        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
        
        http.authenticationProvider(smsCodeAuthenticationProvider)
            .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

6、Spring Security 配置

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private LoginSuccessHandler loginSuccessHandler;
    @Autowired
    private LoginFailureHandler loginFailureHandler;
    @Autowired
    private ValidateCodeFilter validateCodeFilter;
    @Autowired
    private SmsCodeAuthenticationSecurityConfig authenticationSecurityConfig;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore( validateCodeFilter, UsernamePasswordAuthenticationFilter.class )
                .formLogin() 
                .apply(authenticationSecurityConfig);
        super.configure(http);
    }
}

禁用 Spring Security

注解啟動類 ServletInitializer

@EnableAutoConfiguration(exclude = {
        SecurityAutoConfiguration.class
})

RBAC數據模型

表:用戶表,角色表,資源(權限)表,用戶角色關系表,角色資源關系表
資源表:存儲權限控制目標,例如:菜單、按鈕、URL

最佳實踐
1、業務系統,一般權限控制比較簡單,無需RBAC
2、內管系統,需要RBAC,并且系統中有管理RBAC數據的界面
3、資源表的值可以設置為 "對象.操作",例如 "order.delete"表示訂單的刪除權限,"order.delete"表示訂單的刪除權限,"coupon.all"表示優惠券的所有權限
4、前端 根據 RBAC 數據 隱藏 入口(菜單,按鈕)
5、后端 根據 RBAC 數據表存儲的 角色 與 可訪問的 URL 控制訪問

Spring Security 整合 RBAC
1、RBAC 權限判斷 類

// 案例 1
@Component("rbacService")
public class RbacService {
    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        Object principal = authentication.getPrincipal();
        if(principal instanceof UserDetails){
            // 用戶名
            String username = ((UserDetails)principal).getUsername();
            // 根據用戶名 找到 當前用戶可訪問的 url 列表
            Set<String> urls = new HashSet<String>();
            for(String url : urls){
                if(antPathMatcher.match(url, request.getRequestURI())){
                    return true;
                }
            }
        }
        return false;
    }
}
// 案例 2
// 權限的格式為 module.method,判斷當前url是否包含在權限列表里
@Component("rbacService")
public class RbacService {
    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        // 未登錄
        if(authentication.getPrincipal() instanceof String){
            return false;
        }
        Collection<? extends GrantedAuthority> authorityList = authentication.getAuthorities();
        String method = request.getMethod().toLowerCase();
        String path = request.getServletPath();
        for (GrantedAuthority authority : authorityList) {
            // authority 的格式為 module.allow
            // allow取值: get、post、put、delete、all,默認為all
            String[] authoritySegment = authority.getAuthority().split("\\.");
            String module = authoritySegment[0];
            String allow = "all";
            if(authoritySegment.length > 1){
                allow = authoritySegment[1];
            }
            String regexStart = "/admin/" + module + "/";
            String regexEnd = "/admin/" + module;
            if (path.startsWith(regexStart) || path.endsWith(regexEnd) || module.equals("all")) {
                // 權限列表里有的模塊,才允許訪問,且請求方法需要匹配
                if (method.equals("get") || method.equals(allow) || allow.equals("all")) {
                    return true;
                }
            }
        }
        return false;
    }
}

2、Spring Security 配置

        http.authorizeRequests()
                .antMatchers("/*.html").permitAll()  // 對*.html 的請求,放開所有權限
                 // 進行權限判斷,此外仍然會判斷是否認真
                .antMatchers("/manage").access("@rbacService.hasPermission(request, authentication)")   // 放在authenticated()前面
                .anyRequest().authenticated()      // anyRequest()必須放在authorizeRequests的最后,且只能有一個
        super.configure(http);
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內容