一、Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
二、security和springboot也做了深度的契合,所以我们这里使用security来配置相关访问。因为项目可以做前后端的分理,我这里就不讲对于后台处理页面的配置了。这里主要是讲一些针对于纯后端开发,进行security的相关配置,当然只是其中一部分。
三、讲到的点主要有:跨域、认证、加密、权限控制等。
四、实现部分
1、pom.xml需要的依赖(这里只写主要部分,parent等自己按需要导入依赖)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
说明:说明一点,我这里使用的是2.0.0的版本,如何有需要可以自己根据不同的版本进行配置
2、主要的配置部分
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); } }
3、csrf防护
这个我这里不详细讲,主要的目的就是每次访问的时候除了带上自己的访问凭据以外,还需要带上每次csrf的票据。当然这个是会根据具体的会话进行变化的,也就是防止csrf攻击。
如果csrf放开配置方式可以为cookie
即:将.csrf().disable()换成.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
如果存在后端接口忽略的加入:.ignoringAntMatchers("/api/user/login")
访问的时候带上csrf的访问票据,携带方式为下面2种方式。票据的获取方式为第一次访问的时候回通过cookie的方式带入
request:_csrf:票据
header:X-XSRF-TOKEN:票据
4、跨域(配置方式见注入部分)
跨域问题我相信在使用前后台分理的时候肯定会出现这种问题,那么怎么样配置跨域问题呢!这里提供了一种方式(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); } }
5、认证处理以及加密处理
这里的加密方式采用的是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);
这种方式就可以解决异常包装的问题了,这里我们是采用的原生的配置方式。
6、各个切入点(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; } }
其他的就不详细介绍了,基本上都是怎么样去处理,在具体的接入点出现的问题。
7、登录、登出
登录默认的参数为username、password 采用表单方式提交。如果需要修改参数名称可以在loginPage后面加入
.usernameParameter("name")
.passwordParameter("pwd")
说明:默认登录、登出配置的接口不需要实现,默认也是放开的。
8、无需验证访问
在自己开发接口的时候肯定不需要进行权限的访问,这个时候就可以通过配置方式放开具体的请求在.authorizeRequests()配置
.antMatchers("/api/user/register").permitAll()
9、默认会话超时30分钟,可以通过配置修改会话保存时间
server:
servlet:
session:
timeout: 1800s