Spring Security是一个安全框架,底层是通过一系列的过滤器链(filter)实现的,如下图。使用时通过一些配置即可实现用户的身份认证及资源的安全访问。框架提供了一系列的接口可以实现自定义的安全控制和访问拦截。
配置类预览
Spring Security的配置及一些自定义实现
1、 Security配置类(核心)
相关说明请看注释
package com.homemaking.config; import com.homemaking.filter.MyAuthenticationFilter; import com.homemaking.filter.MyUsernamePasswordAuthenticationFilter; import com.homemaking.handler.MyAccessDeniedHandler; import com.homemaking.handler.MyAuthenticationEntryPoint; import com.homemaking.handler.MyAuthenticationFailureHandler; import com.homemaking.handler.MyAuthenticationSuccessHandler; import com.homemaking.myUserDetailsService.MyUserDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity public class SecurityConfigs extends WebSecurityConfigurerAdapter { // 密码加密算法 @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; @Autowired private MyUserDetailsService myUserDetailsService; @Autowired private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler; @Autowired private MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Autowired private MyAuthenticationEntryPoint myAuthenticationEntryPoint; @Autowired private MyAccessDeniedHandler myAccessDeniedHandler; @Autowired private AuthenticationManager authenticationManager; // 密码加密算法,使用框架提供的即可,也可自定义实现 @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } //认证管理器 @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { // 使用框架提供的即可,也可以自己实现 return super.authenticationManagerBean(); } // 登录认证核心配置,允许基于选择匹配在资源级配置基于网络的安全性。 @Override protected void configure(HttpSecurity http) throws Exception { // CORS(Cross Origin Resource Sharing)跨域资源分享 是一种机制,通过在HTTP响应头中加入特定字段限制不同域的资源请求 // CSRF(Cross Site Request Forgery)跨站请求伪造 是一种web攻击手段,通过向服务器发送伪造请求,进行恶意行为的攻击手段 // 这里允许跨域访问,以及禁用csrf方式,因为要使用jwt方式 http.csrf().disable() // 禁用session .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() // 此处是开启表单验证,使用自定义的密码验证器,则这里不需要配置,需要单独设置 // .formLogin() // .loginPage("/login").permitAll() // 登录页面,因为此时还未登录所以需要permitAll放行 // .failureUrl("/login/fail")// 用户密码错误跳转接口 // .defaultSuccessUrl("/login/success",true)// 登录成功跳转接口,true:是指登录成功后,始终跳转到登录成功url // .loginProcessingUrl("/login/transfer") // 登录页面的表单提交到的URL地址,post登录接口,登录验证由系统实现 // .usernameParameter("username") //要认证的用户参数名,默认username // .passwordParameter("password") //要认证的密码参数名,默认password // 认证成功走的方法,在这里返回token // .successHandler(myAuthenticationSuccessHandler) // 认证失败走的方法,直接返回报错 // .failureHandler(myAuthenticationFailureHandler) // .and() // .logout()//配置注销 // .logoutUrl("/logout")//注销接口 // .logoutSuccessUrl("/login/logout").permitAll()//注销成功跳转接口 // .deleteCookies("myCookie") //删除自定义的cookie // .and() // 在这里写授权逻辑 .authorizeRequests() .antMatchers("/login").permitAll() // login是登录接口,直接放行 .antMatchers("/a").access("hasAnyAuthority('a')") .anyRequest().authenticated(); // 任何请求都需要授权,注意顺序 从上至下 // 添加token认证过滤器 http.addFilterBefore(new MyAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class); // 自定义密码登录过滤器(包含生成jwt token) http.addFilterAt(myUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); // 未授权、未登录回调函数,因为和登录无关,所以需要配置在此 http.exceptionHandling() // 未登录 .accessDeniedHandler(myAccessDeniedHandler) // 未授权 .authenticationEntryPoint(myAuthenticationEntryPoint); } // 账号密码验证过滤器配置 @Bean public MyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter() { // 设置自定义过滤器的参数,不设置无法正常返回 MyUsernamePasswordAuthenticationFilter filter = new MyUsernamePasswordAuthenticationFilter("/login", HttpMethod.POST); // 认证管理器 filter.setAuthenticationManager(authenticationManager); // 账号密码认证成功处理器 filter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler); // 账号密码认证失败处理器 filter.setAuthenticationFailureHandler(myAuthenticationFailureHandler); return filter; } // AuthenticationManagerBuilder用来配置全局的认证相关的信息,其实就是AuthenticationProvider和UserDetailsService, // 前者是认证服务提供商,后者是用户详情查询服务。用于通过允许AuthenticationProvider容易地添加来建立认证机制。 @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { // 自定义获取用户详情接口实现,可以从数据库获取,对密码进行加密,也可已使用内存手动写死账号密码然后进行测试 // myUserDetailsService是用户详情接口 auth.userDetailsService(myUserDetailsService).passwordEncoder(bCryptPasswordEncoder); // 使用内存 // auth.inMemoryAuthentication() // .withUser("user") // 账号 // .password("password") // 密码 // .roles("USER") // 权限 // .and() // .withUser("admin") // 账号 // .password("password") // 密码 // .roles("ADMIN", "USER"); // 权限 } // 全局请求忽略规则配置(比如说静态文件,比如说注册页面)、全局HttpFirewall配置、是否debug配置、 // 全局SecurityFilterChain配置、privilegeEvaluator、expressionHandler、securityInterceptor通过实现自定义防火墙定义拒绝请求等的配置设置。 //一般用于配置全局的某些通用事物,例如静态资源等 @Override public void configure(WebSecurity web) throws Exception { // 放行路径(静态资源css、图片等) web.ignoring().antMatchers(); } }
2、自定义token验证过滤器
此处的过滤器可以继承BasicAuthenticationFilter也可以继承OncePerRequestFilter来实现,
OncePerRequestFilter是BasicAuthenticationFilter的抽象父类,BasicAuthenticationFilter默认实现。
package com.homemaking.filter; import com.homemaking.utils.JwtTokenUtils; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Objects; public class MyAuthenticationFilter extends BasicAuthenticationFilter { public MyAuthenticationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { try { // 从token中获取认证信息 Authentication authentication = JwtTokenUtils.getAuthenticationFromToken(request); // 若没有认证信息则直接进入到下一层,走后边策略 if (Objects.isNull(authentication)) { chain.doFilter(request, response); return; } // 将认证信息方法SecurityContextHolder上下文中 SecurityContextHolder.getContext().setAuthentication(authentication); // 参考父类 // this.rememberMeServices.loginSuccess(request, response, authResult); // 记住登录(配置一段时间内面登录) // this.onSuccessfulAuthentication(request, response, authentication); // 空方法 } catch (AuthenticationException var8) { // 认证失败,从上下文删除 SecurityContextHolder.clearContext(); // 参考父类 // this.authenticationEntryPoint.commence(request, response, var8); // return; } chain.doFilter(request, response); } }
JwtToken的工具类
package com.homemaking.utils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import javax.servlet.http.HttpServletRequest; import java.io.Serializable; import java.util.*; public class JwtTokenUtils implements Serializable { private static final long serialVersionUID = 1L; /** * 用户名称 */ private static final String USERNAME = Claims.SUBJECT; /** * 创建时间 */ private static final String CREATED = "created"; /** * 权限列表 */ private static final String AUTHORITIES = "authorities"; /** * 密钥 */ private static final String SECRET = "home"; /** * 有效期24小时 */ private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000; /** * 生成令牌 * * @param authentication * @return 令牌 */ public static String generateToken(Authentication authentication) { Map<String, Object> claims = new HashMap<>(3); claims.put(USERNAME, authentication.getPrincipal());//SecurityUtils.getUsername(authentication) claims.put(CREATED, new Date()); claims.put(AUTHORITIES, authentication.getAuthorities()); return generateToken(claims); } /** * 从数据声明生成令牌 * * @param claims 数据声明 * @return 令牌 */ private static String generateToken(Map<String, Object> claims) { Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME); return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact(); } /** * 从令牌中获取用户名 * * @param token 令牌 * @return 用户名 */ public static String getUsernameFromToken(String token) { String username; try { Claims claims = getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null; } return username; } /** * 根据请求令牌获取登录认证信息 * * @param request * @return 用户名 */ public static Authentication getAuthenticationFromToken(HttpServletRequest request) { Authentication authentication = null; // 获取请求携带的令牌 String tokenStr = JwtTokenUtils.getToken(request); if (tokenStr != null) { // 请求令牌不能为空 if (SecurityContextHolder.getContext().getAuthentication() == null) { // 上下文中Authentication为空 Claims claims = getClaimsFromToken(tokenStr); if (claims == null) { return null; } String username = claims.getSubject(); if (username == null) { return null; } if (isTokenExpired(tokenStr)) { return null; } Object authors = claims.get(AUTHORITIES); List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); if (Objects.nonNull(authors) && authors instanceof List) { for (Object object : (List) authors) { authorities.add(new GrantedAuthority() { private final String authority = (String) ((Map) object).get("authority"); @Override public String getAuthority() { return authority; } }); } } authentication = new UsernamePasswordAuthenticationToken(username, null, authorities) { private String token = tokenStr; public String getToken() { return token; } public void setToken(String token) { this.token = token; } }; // (username, null, authorities, token) } else { if (validateToken(tokenStr, ((User) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername())) { // 如果上下文中Authentication非空,且请求令牌合法,直接返回当前登录认证信息 authentication = SecurityContextHolder.getContext().getAuthentication(); } } } return authentication; } /** * 从令牌中获取数据声明 * * @param token 令牌 * @return 数据声明 */ private static Claims getClaimsFromToken(String token) { Claims claims; try { claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } catch (Exception e) { // token 过期 claims = null; } return claims; } /** * 验证令牌 * * @param token * @param username * @return */ public static Boolean validateToken(String token, String username) { String userName = getUsernameFromToken(token); if (token == null) { return false; } return (userName.equals(username) && !isTokenExpired(token)); } /** * 刷新令牌 * * @param token * @return */ public static String refreshToken(String token) { String refreshedToken; try { Claims claims = getClaimsFromToken(token); claims.put(CREATED, new Date()); refreshedToken = generateToken(claims); } catch (Exception e) { refreshedToken = null; } return refreshedToken; } /** * 判断令牌是否过期 * * @param token 令牌 * @return 是否过期 */ public static Boolean isTokenExpired(String token) { try { Claims claims = getClaimsFromToken(token); Date expiration = claims.getExpiration(); return expiration.before(new Date()); } catch (Exception e) { return false; } } /** * 获取请求token * * @param request * @return */ public static String getToken(HttpServletRequest request) { String token = request.getHeader("Authorization"); String tokenHead = "Bearer "; if (token == null) { token = request.getHeader("token"); } else if (token.contains(tokenHead)) { token = token.substring(tokenHead.length()); } if ("".equals(token)) { token = null; } return token; } }
3、 自定义密码验证过滤器
package com.homemaking.filter; import cn.hutool.json.JSONUtil; import com.homemaking.globalException.HKException; import com.homemaking.globalException.HKExceptionEnum; import com.homemaking.po.CompanyUser; import org.apache.commons.lang3.StringUtils; import org.springframework.http.HttpMethod; import org.springframework.lang.Nullable; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.util.Assert; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; public class MyUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private String usernameParameter = "username"; private String passwordParameter = "password"; private boolean postOnly = true; private String pattern = "/login"; private HttpMethod httpMethod; public MyUsernamePasswordAuthenticationFilter( String pattern, HttpMethod httpMethod) { super(new AntPathRequestMatcher(pattern, httpMethod.toString())); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("不支持的认证方法: " + request.getMethod()); } else { // 表单中获取 String username = this.obtainUsername(request); String password = this.obtainPassword(request); // body中获取 if (StringUtils.isBlank(username)) { String body = this.getBody(request); CompanyUser user = JSONUtil.toBean(body, CompanyUser.class); username = user.getUsername(); password = user.getPassword(); } if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); // 构建身份验证类型(用户名密码类型) UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); // 放到authRequest中 this.setDetails(request, authRequest); // 传入到AuthenticationManager中进行认证 return this.getAuthenticationManager().authenticate(authRequest); } } // 获取post请求中Body体内的数据方法 private String getBody(HttpServletRequest request) { StringBuilder wholeStr = new StringBuilder(); String str; try { BufferedReader br = request.getReader(); while ((str = br.readLine()) != null) { wholeStr.append(str); } } catch (IOException e) { throw new HKException(HKExceptionEnum.FAIL.getCode(), "json解析错误"); } return wholeStr.toString(); } @Nullable protected String obtainPassword(HttpServletRequest request) { return request.getParameter(this.passwordParameter); } @Nullable protected String obtainUsername(HttpServletRequest request) { return request.getParameter(this.usernameParameter); } protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); } public void setUsernameParameter(String usernameParameter) { Assert.hasText(usernameParameter, "Username parameter must not be empty or null"); this.usernameParameter = usernameParameter; } public void setPasswordParameter(String passwordParameter) { Assert.hasText(passwordParameter, "Password parameter must not be empty or null"); this.passwordParameter = passwordParameter; } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getUsernameParameter() { return this.usernameParameter; } public final String getPasswordParameter() { return this.passwordParameter; } }
4、自定义未登录处理程序
package com.homemaking.handler; import com.homemaking.globalException.HKException; import com.homemaking.globalException.HKExceptionEnum; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Service; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author: zhi * @description: 未登录处理程序 * @date: 2022/11/11 13:34 */ @Service public class MyAccessDeniedHandler implements AccessDeniedHandler { // 未定登录或登录过期后会走的接口,可以在接口里做一些业务逻辑等 @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { // HKException我自定义的异常返回类,HKExceptionEnum自定义错误的枚举 throw new HKException(HKExceptionEnum.NOT_LOGGED_IN.getCode(), e.getMessage()); } }
5、 自定义未授权管理器
package com.homemaking.handler; import cn.hutool.json.JSONUtil; import com.homemaking.common.result.Result; import com.homemaking.globalException.HKExceptionEnum; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Service; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Service public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint { // 未授权的接口,用户访问资源,但未给用户授权,会走此接口 @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { // Result是我自定义的统一返回的类。 Result<Object> result = Result.setException(HKExceptionEnum.NOT_AUTHORIZED, e); String json = JSONUtil.parse(result).toString(); // 此处使用的是Servlet的写出栈方法直接写到Response里。 httpServletResponse.getWriter().write(json); // throw new HKException(HKExceptionEnum.NOT_AUTHORIZED.getCode(), e.getMessage()); } }
6、自定义认证失败的回调处理器
package com.homemaking.handler; import com.homemaking.globalException.HKException; import com.homemaking.globalException.HKExceptionEnum; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Service; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Service public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { // 实现此方法,在认证失败后可调用此处逻辑 @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { // HKException我自定义的异常返回类,HKExceptionEnum自定义错误的枚举 throw new HKException(HKExceptionEnum.AUTHENTICATION_FAILURE.getCode(), e.getMessage()); } }
7、 自定义认证成功的回调处理器
package com.homemaking.handler; import cn.hutool.core.map.MapUtil; import cn.hutool.json.JSONUtil; import com.homemaking.common.result.Result; import com.homemaking.utils.JwtTokenUtils; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Service; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Map; @Service public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { // 认证成功后可在此处生成token发送给前端,以及作一些其他处理 @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { // jwt存放用户信息和认证信息 String token = JwtTokenUtils.generateToken(authentication); Object userInfo = authentication.getPrincipal(); Map<String, Object> map = MapUtil.builder("userInfo", userInfo).put("token", token).build(); Result<Map<String, Object>> stringResult = Result.setData(map); String s = JSONUtil.toJsonStr(stringResult); response.getWriter().write(s); } }
8、获取用户详情接口
package com.homemaking.myUserDetailsService; import com.homemaking.SysClient; import com.homemaking.common.result.Result; import com.homemaking.dto.CompanyUserDTO; import com.homemaking.globalException.HKExceptionEnum; import com.homemaking.pojo.dto.UserSecurityDTO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @Service public class MyUserDetailsService implements UserDetailsService { @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; @Autowired private SysClient sysClient; // 获取用户详情接口,这里是使用openfeign远程调用 @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Result<CompanyUserDTO> result = sysClient.findByUsername(username); if (result.getCode() != HKExceptionEnum.SUCCESS.getCode()) { throw new UsernameNotFoundException(result.getMessage()); } CompanyUserDTO dto = result.getData(); return UserSecurityDTO.transUserSecurityDTO(dto); } }
由于安全框架的强大功能还有很多可以使用的配置以及自定义的实现来达到满足自己系统服务的要求,比如记住 rememberMe (翻译为‘记住我’,实际上是一段时间内可以免登录,这个很多人有个误区是前端实现的,实际上是后端实现的)。