1、Spring Security是一個可以爲基於Spring的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組能夠在Spring應用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反轉Inversion of Control ,DI:Dependency Injection 依賴注入)和AOP(面向切面編程)功能,爲應用系統提供聲明式的安全訪問控制功能,減小了爲企業系統安全控制編寫大量重複代碼的工做。html
2、security和springboot也作了深度的契合,因此咱們這裏使用security來配置相關訪問。由於項目能夠作先後端的分理,我這裏就不講對於後臺處理頁面的配置了。這裏主要是講一些針對於純後端開發,進行security的相關配置,固然只是其中一部分。java
3、講到的點主要有:跨域、認證、加密、權限控制等。web
4、實現部分spring
一、pom.xml須要的依賴(這裏只寫主要部分,parent等本身按須要導入依賴)編程
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
說明:說明一點,我這裏使用的是2.0.0的版本,如何有須要能夠本身根據不一樣的版本進行配置json
二、主要的配置部分後端
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.security.web.csrf.CsrfFilter; import org.springframework.web.filter.CorsFilter; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfiguration extends WebSecurityConfigurerAdapter { //跨域 @Autowired private CorsFilter corsFilter; //認證處理類 @Autowired private DaoAuthenticationProvider daoAuthenticationProvider; //認證成功 @Autowired private AuthenticationSuccessHandler successHandler; //認證失敗 @Autowired private AuthenticationFailureHandler failureHandler; //登出成功 @Autowired private LogoutSuccessHandler logoutSuccessHandler; @Autowired private AccessDeniedHandler deniedHandler; //認證EntryPoint @Autowired private AuthenticationEntryPoint entryPoint; @Override protected void configure(AuthenticationManagerBuilder builder) throws Exception { builder.authenticationProvider(daoAuthenticationProvider); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring() .antMatchers(HttpMethod.OPTIONS, "/api/**") .antMatchers("/swagger-ui.html") .antMatchers("/webjars/**") .antMatchers("/swagger-resources/**") .antMatchers("/v2/**"); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .disable() .addFilterBefore(corsFilter, CsrfFilter.class) .exceptionHandling() .authenticationEntryPoint(entryPoint) .accessDeniedHandler(deniedHandler) .and() .authorizeRequests() .anyRequest().authenticated() .and() .formLogin().loginPage("/api/user/login") .successHandler(successHandler) .failureHandler(failureHandler) .and() .logout().logoutUrl("/api/user/logout") .logoutSuccessHandler(logoutSuccessHandler) .and() .headers() .frameOptions() .disable() .and() .sessionManagement().maximumSessions(1800); } }
三、csrf防禦api
這個我這裏不詳細講,主要的目的就是每次訪問的時候除了帶上本身的訪問憑據之外,還須要帶上每次csrf的票據。固然這個是會根據具體的會話進行變化的,也就是防止csrf攻擊。跨域
若是csrf放開配置方式能夠爲cookie安全
即:將.csrf().disable()換成.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
若是存在後端接口忽略的加入:.ignoringAntMatchers("/api/user/login")
訪問的時候帶上csrf的訪問票據,攜帶方式爲下面2種方式。票據的獲取方式爲第一次訪問的時候回經過cookie的方式帶入
request:_csrf:票據
header:X-XSRF-TOKEN:票據
四、跨域(配置方式見注入部分)
跨域問題我相信在使用先後臺分理的時候確定會出現這種問題,那麼怎麼樣配置跨域問題呢!這裏提供了一種方式(CorsFilter.class)
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; import java.util.ArrayList; import java.util.List; @Configuration public class CorsFilterConfiguration { @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration corsConfiguration = new CorsConfiguration(); List<String> allowedOrigins = new ArrayList<>(); allowedOrigins.add("*"); List<String> allowedMethods = new ArrayList<>(); allowedMethods.add("*"); List<String> allowedHeaders = new ArrayList<>(); allowedHeaders.add("*"); List<String> exposedHeaders = new ArrayList<>(); exposedHeaders.add("Link"); exposedHeaders.add("X-Total-Count"); corsConfiguration.setAllowedOrigins(allowedOrigins); corsConfiguration.setAllowedMethods(allowedMethods); corsConfiguration.setAllowedHeaders(allowedHeaders); corsConfiguration.setExposedHeaders(exposedHeaders); corsConfiguration.setAllowCredentials(true); corsConfiguration.setMaxAge(1800L); source.registerCorsConfiguration("/api/**", corsConfiguration);return new CorsFilter(source); } }
五、認證處理以及加密處理
這裏的加密方式採用的是springsecurity提供的一種加密方式:BCryptPasswordEncoder(hash、同一密碼加密不同的密鑰),認證配置見builder部分
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @Configuration public class PasswordEncoderConfiguration { /** * 密碼加密 */ @Bean public BCryptPasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
import com.cetc.domain.Role; import com.cetc.domain.User; import com.cetc.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.List; @Service @Transactional public class AuthDetailsService implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if (user == null){ throw new UsernameNotFoundException("用戶不存在!"); } List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>(); List<Role> roles = user.getRoles(); if (roles != null && !roles.isEmpty()) { roles.stream().forEach(role -> simpleGrantedAuthorities.add(new SimpleGrantedAuthority(role.getRoleType()))); } return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), simpleGrantedAuthorities); } }
說明:這裏的UsernameNotFoundException若是是默認配置,是不能被處理類所捕獲的。緣由:DaoAuthenticationProvider父類AbstractUserDetailsAuthenticationProvider中hideUserNotFoundExceptions爲true
解決方式從新配置:DaoAuthenticationProvider
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @Configuration public class CustomDaoAuthenticationProvider { @Autowired private AuthDetailsService authDetailsService; @Autowired private BCryptPasswordEncoder passwordEncoder; @Bean public DaoAuthenticationProvider daoAuthenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(authDetailsService); provider.setPasswordEncoder(passwordEncoder); provider.setHideUserNotFoundExceptions(false); return provider; } }
而後修改builder.userDetailsService(authDetailsService).passwordEncoder(passwordEncoder);爲builder.authenticationProvider(provider);
這種方式就能夠解決異常包裝的問題了,這裏咱們是採用的原生的配置方式。
六、各個切入點(AuthenticationEntryPoint、AccessDeniedHandler、AuthenticationSuccessHandler、AuthenticationFailureHandler、LogoutSuccessHandler)五個切入點,做用就是在對應操做事後,能夠根據具體的切入點進行相應異常的處理
import com.alibaba.fastjson.JSONObject; import com.cetc.constant.SystemErrorCode; import com.cetc.dto.MenuDTO; import com.cetc.result.ResponseMsg; import com.cetc.service.IUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import java.util.List; @Configuration public class CustomHandlerConfiguration { @Autowired private IUserService userService; /** * 訪問接入點處理 * @return */ @Bean public AuthenticationEntryPoint authenticationEntryPoint() { AuthenticationEntryPoint entryPoint = (request, response, e) -> { ResponseMsg<String> responseMsg = new ResponseMsg<>(); responseMsg.setStatus(false); responseMsg.setBody(e.getMessage()); responseMsg.setErrorCode(SystemErrorCode.NO_LOGIN); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter().write(JSONObject.toJSONString(responseMsg)); }; return entryPoint; } /** * 接入事後問題處理 * @return */ @Bean public AccessDeniedHandler accessDeniedHandler() { AccessDeniedHandler accessDeniedHandler = (request, response, e) -> { ResponseMsg<String> responseMsg = new ResponseMsg<>(); responseMsg.setStatus(false); responseMsg.setBody(e.getMessage()); responseMsg.setErrorCode(SystemErrorCode.NO_PERMISSION); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter().write(JSONObject.toJSONString(responseMsg)); }; return accessDeniedHandler; } /** * 登陸成功後的處理 * @return */ @Bean public AuthenticationSuccessHandler authenticationSuccessHandler() { AuthenticationSuccessHandler authenticationSuccessHandler = (request, response, authentication) -> { //返回數據 ResponseMsg<List<MenuDTO>> responseMsg = new ResponseMsg<>(); responseMsg.setBody(userService.getMenus()); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter().write(JSONObject.toJSONString(responseMsg)); }; return authenticationSuccessHandler; } /** * 登陸失敗後的處理 * @return */ @Bean public AuthenticationFailureHandler authenticationFailureHandler() { AuthenticationFailureHandler authenticationFailureHandler = (request, response, e) -> { ResponseMsg<String> responseMsg = new ResponseMsg<>(); responseMsg.setStatus(false); responseMsg.setBody(e.getMessage()); responseMsg.setErrorCode(SystemErrorCode.ACCOUNT_ERROR); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter().write(JSONObject.toJSONString(responseMsg)); }; return authenticationFailureHandler; } /** * 登出成功後的處理 * @return */ @Bean public LogoutSuccessHandler logoutSuccessHandler() { LogoutSuccessHandler logoutSuccessHandler = (request, response, authentication) -> { ResponseMsg<String> responseMsg = new ResponseMsg<>(); responseMsg.setStatus(true); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter().write(JSONObject.toJSONString(responseMsg)); }; return logoutSuccessHandler; } }
其餘的就不詳細介紹了,基本上都是怎麼樣去處理,在具體的接入點出現的問題。
七、登陸、登出
登陸默認的參數爲username、password 採用表單方式提交。若是須要修改參數名稱能夠在loginPage後面加入
.usernameParameter("name")
.passwordParameter("pwd")
說明:默認登陸、登出配置的接口不須要實現,默認也是放開的。
八、無需驗證訪問
在本身開發接口的時候確定不須要進行權限的訪問,這個時候就能夠經過配置方式放開具體的請求在.authorizeRequests()配置
.antMatchers("/api/user/register").permitAll()
九、默認會話超時30分鐘,能夠經過配置修改會話保存時間
server:
servlet:
session:
timeout: 1800s