spring security 是企业应用系统的权限管理框架,应用安全性包括用户认证和用户授权两部分,用户认证一般要求用户提供用户名和密码,系统通过校验用户名和密码完成认证过程,用户授权是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。spring security 的主要核心功能为认证和授权。
1.WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。
2.SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。
3.HeaderWriterFilter:用于将头信息加入响应中。
4.CsrfFilter:用于处理跨站请求伪造。
5.LogoutFilter:用于处理退出登录。
6.UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。
7.DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
8.BasicAuthenticationFilter:检测和处理 http basic 认证。
9.RequestCacheAwareFilter:用来处理请求的缓存。
10.SecurityContextHolderAwareRequestFilter:主要是包装请求对象request。
11.AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。
12.SessionManagementFilter:管理 session 的过滤器
13.ExceptionTranslationFilter:处理 AccessDeniedException 和
AuthenticationException 异常。
14.FilterSecurityInterceptor:可以看做过滤器链的出口。
RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
org.springframework.boot
spring-boot-starter-security
2.3.1.RELEASE
io.jsonwebtoken
jjwt
0.9.1
public class SnailSecurityAutoConfig extends WebSecurityConfigurerAdapter {
private final CustomerSecurityProperties customerSecurityProperties;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final JwtAuthorizationTokenFilter jwtAuthorizationTokenFilter;
private final CustomerUsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter;
private final CustomerFilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource;
private final CustomerAccessDecisionManager accessDecisionManager;
public SnailSecurityAutoConfig(JwtAuthorizationTokenFilter jwtAuthorizationTokenFilter, CustomerSecurityProperties customerSecurityProperties, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint, JwtAccessDeniedHandler jwtAccessDeniedHandler, CustomerUsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter, CustomerFilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource, CustomerAccessDecisionManager accessDecisionManager) {
this.customerSecurityProperties = customerSecurityProperties;
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
this.jwtAuthorizationTokenFilter = jwtAuthorizationTokenFilter;
this.usernamePasswordAuthenticationFilter = usernamePasswordAuthenticationFilter;
this.filterInvocationSecurityMetadataSource = filterInvocationSecurityMetadataSource;
this.accessDecisionManager = accessDecisionManager;
}
/**
* 忽略拦截url或静态资源文件夹 - web.ignoring(): 会直接过滤该url - 将不会经过Spring Security过滤器链
* http.permitAll(): 不会绕开springsecurity验证,相当于是允许该路径通过
*
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) {
//放行swagger
web.ignoring().antMatchers(HttpMethod.GET,
"/v2/api-docs",
"/swagger-resources",
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui.html/**",
"/webjars/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry authorizeRequests = http.authorizeRequests();
for (CustomerSecurityProperties.InterceptPath interceptPath : customerSecurityProperties.getInterceptPath()){
authorizeRequests.antMatchers(interceptPath.getUrl()).hasAnyRole(interceptPath.getRole().split("[,;]]"));
}
http
.csrf().disable() //关闭spring自带的csrf
.authorizeRequests() //验证请求
.antMatchers(customerSecurityProperties.getPublicPath()).permitAll() //配置放行url
.anyRequest().authenticated() //其余需要验证
.withObjectPostProcessor(new ObjectPostProcessor() {
@Override
public O postProcess(O object) {
object.setAccessDecisionManager(accessDecisionManager);
object.setSecurityMetadataSource(filterInvocationSecurityMetadataSource);
return object;
}
}) //鉴权配置
.and()
.logout() //登出
.logoutSuccessHandler(new CustomerLogoutSuccessHandler()) //成功登出handler
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint) //配置匿名访问无权限url 的handler
.accessDeniedHandler(jwtAccessDeniedHandler) //配置登录后访问无权限url 的handler
.and()
.sessionManagement() //session管理
.sessionCreationPolicy(SessionCreationPolicy.STATELESS); //禁用session
http.addFilterAt(usernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); //登录过滤器
http.addFilterBefore(jwtAuthorizationTokenFilter, CustomerUsernamePasswordAuthenticationFilter.class);//token过滤验证
}
}
configure(WebSecurity web) 配置忽略拦截将不会经过Spring Security过滤器链,
http.permitAll(): 不会绕开springsecurity验证,相当于是允许该路径通过
@Data
@Configuration
@ConfigurationProperties(prefix = "snail.security")
public class CustomerSecurityProperties {
private Jwt jwt;
private InterceptPath[] interceptPath = null;
private String[] publicPath = {
"/login",
};
@Data
public static class Jwt{
String subject = "uAuthentication";
String header = "Authorization";
String tokenStartWith = "Bearer";
String base64Secret = "jdgdjguadgliwiweiuqiahdiwuhdiahdiwuqidihshdkhiwqheiuwdwhoqdwqdjgtfghdg";
Long tokenValidityInSeconds = 7200000L;
}
@Data
public static class InterceptPath{
private String url;
/**
*角色用,或者;分割
*/
private String role;
}
}
public class CustomerLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("Application/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(ResponseVO.success()));
// authentication.setAuthenticated(false);
}
}
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@SneakyThrows
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpConstance.STATUS_DENY);
ResponseUtil.writeToResponse(response, JSON.toJSONString(ResponseVO.error(HttpConstance.STATUS_DENY,"禁止访问")));
}
}
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger log= LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
log.error("JwtAuthenticationEntryPoint{}","没有凭证");
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(HttpConstance.STATUS_NO_AUTH);
PrintWriter pWriter = httpServletResponse.getWriter();
pWriter.write(JSON.toJSONString(ResponseVO.error(HttpConstance.STATUS_NO_AUTH,"没有凭证,验证失败")));
pWriter.flush();
pWriter.close();
}
}
@Slf4j
@Data
public class CustomerAuthenticationProvider implements AuthenticationProvider {
private PasswordEncoder passwordEncoder;
/**
* The password used to perform
* {@link PasswordEncoder#matches(CharSequence, String)} on when the user is
* not found to avoid SEC-2056. This is necessary, because some
* {@link PasswordEncoder} implementations will short circuit if the password is not
* in a valid format.
*/
private volatile String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
private JwtTokenUtil jwtTokenUtil;
public CustomerAuthenticationProvider(PasswordEncoder passwordEncoder, UserDetailsService userDetailsService , JwtTokenUtil jwtTokenUtil) {
setJwtTokenUtil(jwtTokenUtil);
setPasswordEncoder(passwordEncoder);
setUserDetailsService(userDetailsService);
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
NormalUser userDetails = (NormalUser) userDetailsService.loadUserByUsername(username);
log.debug(String.format("username:%s,password:%s",username,password));
//验证密码
Boolean isValid = PasswordUtil.isValid(password, userDetails.getPassword(), username, getPasswordEncoder());
if (!isValid) {
throw new BadCredentialsException("密码错误!");
}
//前后端分离,token生成
String token = jwtTokenUtil.createToken(userDetails.getUsername(), userDetails.getAuthorities().toString());
//String token = jwtTokenUtil.generateToken(userDetails);
userDetails.setToken(token);
return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
}
//支持的验证方式是用户名密码验证及其子类的验证方式
@Override
public boolean supports(Class> authentication) {
return CustomerUsernamePasswordAuthenticationFilter.class.isAssignableFrom(authentication);
}
}
@Slf4j
public class CustomerAuthenticationManager implements AuthenticationManager {
private CustomerAuthenticationProvider authenticationProvider;
public CustomerAuthenticationManager(CustomerAuthenticationProvider authenticationProvider) {
this.authenticationProvider = authenticationProvider;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
AuthenticationProvider authenticationProvider = getAuthenticationProvider();
Authentication result = authenticationProvider.authenticate(authentication);
if(result != null)log.info("CustomerAuthenticationManager{}",result.toString());
else{
log.info("CustomerAuthenticationManager{}","Authentication failed");
}
if (Objects.nonNull(result)){
return result;
}
throw new ProviderNotFoundException("Authentication failed");
}
public CustomerAuthenticationProvider getAuthenticationProvider() {
return authenticationProvider;
}
public void setAuthenticationProvider(CustomerAuthenticationProvider authenticationProvider) {
this.authenticationProvider = authenticationProvider;
}
}
@Slf4j
public class CustomerUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
private CustomerSecurityProperties customerSecurityProperties;
private JwtTokenUtil jwtTokenUtil;
public CustomerUsernamePasswordAuthenticationFilter(CustomerAuthenticationManager authenticationManager, CustomerSecurityProperties customerSecurityProperties, JwtTokenUtil jwtTokenUtil) {
super(new AntPathRequestMatcher("/login", "PUT"));
setAuthenticationManager(authenticationManager);
setCustomerSecurityProperties(customerSecurityProperties);
setJwtTokenUtil(jwtTokenUtil);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equalsIgnoreCase("PUT")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
log.info("CustomerUsernamePasswordAuthenticationFilter {}", " inter attemptAuthentication");
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
log.debug("CustomerUsernamePasswordAuthenticationFilter{}", String.format("username:%s,password:%s", username, password));
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password, null);
// Allow subclasses to set the "details" property
authRequest.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
@SneakyThrows
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
NormalUser userDetail = (NormalUser) authResult.getPrincipal();
Collection extends GrantedAuthority> authorities = authResult.getAuthorities();
//String token = jwtTokenUtil.createToken(user.getUsername(), authorities.toString());
log.info("successfulAuthentication {}",userDetail.getToken());
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setHeader("token", customerSecurityProperties.getJwt().getHeader() + userDetail.getToken());
ResponseUtil.writeToResponse(response, JSON.toJSONString(ResponseVO.success("登录成功")));
}
@SneakyThrows
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
//super.unsuccessfulAuthentication(request, response, failed);
String responseData = "";
if (failed instanceof AccountExpiredException) {
responseData = "账号过期";
} else if (failed instanceof BadCredentialsException) {
responseData = "账号或密码错误";
} else if (failed instanceof CredentialsExpiredException) {
responseData = "密码过期";
} else if (failed instanceof DisabledException) {
responseData = "账号不可用";
} else if (failed instanceof LockedException) {
responseData = "账号被锁定";
} else if (failed instanceof InternalAuthenticationServiceException) {
responseData = "用户不存在";
} else {
responseData = "未知错误";
}
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
ResponseUtil.writeToResponse(response, JSON.toJSONString(ResponseVO.success("登录失败" + responseData)));
}
/**
* Enables subclasses to override the composition of the password, such as by
* including additional values and a separator.
*
* This might be used for example if a postcode/zipcode was required in addition to
* the password. A delimiter such as a pipe (|) should be used to separate the
* password and extended value(s). The AuthenticationDao
will need to
* generate the expected password in a corresponding manner.
*
*
* @param request so that request attributes can be retrieved
* @return the password that will be presented in the Authentication
* request token to the AuthenticationManager
*/
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}
/**
* Enables subclasses to override the composition of the username, such as by
* including additional values and a separator.
*
* @param request so that request attributes can be retrieved
* @return the username that will be presented in the Authentication
* request token to the AuthenticationManager
*/
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}
/**
* Sets the parameter name which will be used to obtain the username from the login
* request.
*
* @param usernameParameter the parameter name. Defaults to "username".
*/
public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}
/**
* Sets the parameter name which will be used to obtain the password from the login
* request..
*
* @param passwordParameter the parameter name. Defaults to "password".
*/
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}
/**
* Defines whether only HTTP POST requests will be allowed by this filter. If set to
* true, and an authentication request is received which is not a POST request, an
* exception will be raised immediately and authentication will not be attempted. The
* unsuccessfulAuthentication() method will be called as if handling a failed
* authentication.
*
* Defaults to true but may be overridden by subclasses.
*/
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return usernameParameter;
}
public final String getPasswordParameter() {
return passwordParameter;
}
public AuthenticationManager getAuthenticationManager() {
return super.getAuthenticationManager();
}
@Autowired
public void setAuthenticationManager(CustomerAuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}
public CustomerSecurityProperties getCustomerSecurityProperties() {
return customerSecurityProperties;
}
public void setCustomerSecurityProperties(CustomerSecurityProperties customerSecurityProperties) {
this.customerSecurityProperties = customerSecurityProperties;
}
public JwtTokenUtil getJwtTokenUtil() {
return jwtTokenUtil;
}
public void setJwtTokenUtil(JwtTokenUtil jwtTokenUtil) {
this.jwtTokenUtil = jwtTokenUtil;
}
}
public class CustomerFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private final IUrlService iUrlService ;
public CustomerFilterInvocationSecurityMetadataSource(IUrlService iUrlService) {
this.iUrlService = iUrlService;
}
@Override
public Collection getAttributes(Object object) throws IllegalArgumentException {
Map> allRoleResource = new HashMap<>();
for (NormalUrl normalUrl: iUrlService.loadAll() ){
allRoleResource.put(new AntPathRequestMatcher(normalUrl.getUrl()), SecurityConfig.createList(normalUrl.getRole().split("[,;]")));
}
FilterInvocation filterInvocation = (FilterInvocation)object;
HttpServletRequest request = filterInvocation.getRequest();
for (Map.Entry> entry : allRoleResource.entrySet()){
if (entry.getKey().matches(request)){
return entry.getValue();
}
}
return null;
}
@Override
public Collection getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
@Slf4j
public class CustomerAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
Iterator iterator = configAttributes.iterator();
if(!iterator.hasNext()){
return;
}
if(authentication == null){
throw new DeniedAccessException( “authentication 为空,没有访问权限”,“CustomerAccessDecisionManager”);
}
while (iterator.hasNext()){
ConfigAttribute configAttribute = iterator.next();
String needCode = configAttribute.getAttribute();
Collection extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority grantedAuthority : authorities){
log.info("AccessDecisionManager{} {}",grantedAuthority.getAuthority(),"ROLE_"+needCode);
if(StringUtils.equals(grantedAuthority.getAuthority(),"ROLE_"+needCode)){
return;
}
}
}
throw new DeniedAccessException( "authentication 没有访问权限","CustomerAccessDecisionManager");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return false;
}
@Override
public boolean supports(Class> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
public class PasswordUtil {
public static Boolean isValid(String password, String realPassword, String salt, PasswordEncoder passwordEncoder) {
if (password == null || realPassword == null) {
throw new IllegalArgumentException("密码参数错误");
}
if (passwordEncoder == null || passwordEncoder instanceof NoOpPasswordEncoder) {
return Objects.equals(realPassword, password);
}
return passwordEncoder.matches(password + salt ,realPassword);
}
}
public class ResponseUtil {
public static void writeToResponse(HttpServletResponse response, Object message) throws IOException, NoSuchMethodException {
if(response == null || message == null){
throw new OperationFailedException("空异常","httpUtil");
}
PrintWriter writer = response.getWriter();
try {
if(message instanceof String ){
writer.write((String)message);
}else if(message instanceof Integer){
writer.write((Integer) message);
}else if(message instanceof char[]){
writer.write((char[])message);
}else{
writer.write(message.toString());
}
writer.flush();
}catch (Exception e){
e.printStackTrace();
throw e;
}finally {
writer.close();
}
}
}