本篇文章环境:Spring Boot + Mybatis + Spring Security + Redis + JWT
预期成果:实现具有验证码校验、RBAC 权限控制的前后端分离项目
注意:为什么要使用 Json Web Token(JWT)?这是因为前后端分离项目中,前端与后端之间的通信采用 RESTFul API 的交互方式进行交互。这种前后端分离的交互是无状态的交互方式,所以,每次交互都必须进行身份验证。而传统方案是根据用户信息生成token,将token 存入浏览器 cookie(或存入数据库),之后每次请求都会带上这个 cookie,由后端根据这个 cookie 来查询用户并验证是否过期。这种方案存在很多的问题,由于 cookie 是可以被 Javascript 读取的,这会导致用户 token 泄露。
数据库设计Web 的安全控制一般分为两个部分,一个是认证,一个是授权。认证即判断是否为合法用户,简单的说就是登录。用户名和密码匹配成功即认证成功。授权是基于已认证的前提下,根据用户的不同权限,开放不同的资源(本文简单化处理,认为资源就是 API,实际的资源可能包括菜单、静态图片等)。一般 RBAC 权限控制有三层,即:用户<–>角色<–>权限,用户与角色是多对多,角色和权限也是多对多,最后由权限控制资源(URL)的访问。本文为了便于处理,在数据库设计时对表内的字段进行了简化。
在这里我们先暂时不考虑权限,只考虑用户<–>角色<–>资源。(源码被注释的部分中含有用户、角色、权限三层控制)
认证管理在这一步中,我们需要自定义 UserDetailsService ,将用户信息和权限注入进来,为后面的授权做准备。
在实现UserDetailsService之后,需要重写 loadUserByUsername 方法,参数是用户输入的用户名。返回值是UserDetails,这是一个接口,一般使用它的子类org.springframework.security.core.userdetails.User,它有三个参数,分别是用户名、密码和权限集。(实际开发中,我们可以将实体类中的 User 继承org.springframework.security.core.userdetails.User以满足更多需求)
并且实际应用中,为了减少对数据库的访问次数,我们通常会将权限集放入缓存中,下次可以直接从缓存中获取,可以有效提高效率。
UserDetailsService 实现类package com.security.service; import com.security.mapper.APIMapper; import com.security.mapper.AuthoritiesMapper; import com.security.mapper.RoleMapper; import com.security.mapper.UserMapper; import com.security.pojo.SysUser; import com.security.utils.RedisUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; 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.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; @Service("userDetailsService") public class UserDetailServiceImpl implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Autowired private UserMapper userMapper; @Autowired private RoleMapper roleMapper; @Autowired private AuthoritiesMapper authoritiesMapper; @Autowired private RedisUtils redisUtils; //自定义的登录逻辑 @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser user = userMapper.queryUserByUsername(username); //根据用户名去数据库进行查询,如不存在则抛出异常 if (user == null){ throw new UsernameNotFoundException("用户不存在"); } List部分 mapper 接口和具体实现authorities = new ArrayList<>(); //方法一:使用用户、角色、资源建立关系,直接使用角色控制权限 List codeList = roleMapper.queryUserRole(user.getUsername()); //添加权限信息进入缓存 redisUtils.set(username, StringUtils.join(codeList,","),60 * 60); //方法二:添加权限(资源表),通过建立用户、角色、权限、资源之间的关系,使用"权限"实现按钮级别的权限控制 // List codeList = authoritiesMapper.queryAuthoritiesList(user.getUsername()); codeList.forEach(code ->{ SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(code); authorities.add(simpleGrantedAuthority); }); return new User(username, user.getPassword(), authorities); } }
package com.security.mapper; import com.security.pojo.Role; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; import java.util.List; @Mapper @Repository public interface RoleMapper { ListqueryUserRole(@Param("username")String username); List selectListByUrl(String url); }
package com.security.mapper; import com.security.pojo.SysUser; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; @Mapper @Repository public interface UserMapper { SysUser queryUserByUsername(@Param("username")String username); }
select * from sys_user where username = #{username}
接着,创建一个配置类并继承自 WebSecurityConfigurerAdapter,并重写 configure(AuthenticationManagerBuilder auth) 方法就可以完成简单的登录认证了。在这一步里,我们需要自定义两个处理器,分别是成功处理器和失败处理器,以及统一的前后端通信消息体格式。
成功处理器package com.security.config; import com.security.common.ResponseBody; import com.security.utils.JwtUtils; import com.security.utils.ResponseBodyUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class AjaxAuthenticationSuccessHandler extends JSONAuthentication implements AuthenticationSuccessHandler { //JWT处理工具类 @Autowired private JwtUtils jwtUtils; @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { ResponseBody responseBody = ResponseBodyUtil.success(); //生成jwt Token String jwt = jwtUtils.generateToken(authentication.getName()); httpServletResponse.setHeader(jwtUtils.getHeader(), jwt); //继承封装的输出JSON格式类,并调用父类方法即可 this.WriteJSON(httpServletRequest,httpServletResponse,responseBody); } }失败处理器
package com.security.config; import com.security.common.ResponseBody; import com.security.common.ResponseCode; import com.security.utils.ResponseBodyUtil; import org.springframework.security.authentication.*; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class AjaxAuthenticationFailureHandler extends JSONAuthentication implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { ResponseBody responseBody = null; if (e instanceof AccountExpiredException) { //账号过期 responseBody = ResponseBodyUtil.fail(ResponseCode.USER_ACCOUNT_EXPIRED); } else if (e instanceof BadCredentialsException) { //密码错误 responseBody = ResponseBodyUtil.fail(ResponseCode.USER_CREDENTIALS_ERROR); } else if (e instanceof CredentialsExpiredException) { //密码过期 responseBody = ResponseBodyUtil.fail(ResponseCode.USER_CREDENTIALS_EXPIRED); } else if (e instanceof DisabledException) { //账号不可用 responseBody = ResponseBodyUtil.fail(ResponseCode.USER_ACCOUNT_DISABLE); } else if (e instanceof LockedException) { //账号锁定 responseBody = ResponseBodyUtil.fail(ResponseCode.USER_ACCOUNT_LOCKED); } else if (e instanceof InternalAuthenticationServiceException) { //用户不存在 responseBody = ResponseBodyUtil.fail(ResponseCode.USER_ACCOUNT_NOT_EXIST); }else{ //其他错误 responseBody = ResponseBodyUtil.fail(ResponseCode.COMMON_FAIL); } //继承封装的输出JSON格式类,并调用父类方法即可 this.WriteJSON(httpServletRequest,httpServletResponse,responseBody); } }封装的 JSonAuthentication 抽象类
该抽象类主要是对处理器内都需要实现的一些功能的一个封装
package com.security.config; import com.alibaba.fastjson.JSON; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; public abstract class JSONAuthentication { protected void WriteJSON(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object obj) throws IOException, ServletException { //设置编码格式 httpServletResponse.setContentType("text/json;charset=utf-8"); //处理跨域问题 httpServletResponse.setHeader("Access-Control-Allow-Origin", "*"); httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET"); //输出JSON PrintWriter out = httpServletResponse.getWriter(); out.write(JSON.toJSONString(obj)); out.flush(); out.close(); } }封装的消息体
package com.security.common; import lombok.Data; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import java.io.Serializable; @Data @Getter @Setter @NoArgsConstructor public class ResponseBody响应码枚举类implements Serializable { private Boolean success; private Integer statusCode; private String msg; private T data; public ResponseBody(boolean success) { this.success = success; this.statusCode = success ? ResponseCode.SUCCESS.getCode() : ResponseCode.COMMON_FAIL.getCode(); this.msg = success ? ResponseCode.SUCCESS.getMessage() : ResponseCode.COMMON_FAIL.getMessage(); } public ResponseBody(boolean success, ResponseCode resultEnum) { this.success = success; this.statusCode = success ? ResponseCode.SUCCESS.getCode() : (resultEnum == null ? ResponseCode.COMMON_FAIL.getCode() : resultEnum.getCode()); this.msg = success ? ResponseCode.SUCCESS.getMessage() : (resultEnum == null ? ResponseCode.COMMON_FAIL.getMessage() : resultEnum.getMessage()); } public ResponseBody(boolean success, T data) { this.success = success; this.statusCode = success ? ResponseCode.SUCCESS.getCode() : ResponseCode.COMMON_FAIL.getCode(); this.msg = success ? ResponseCode.SUCCESS.getMessage() : ResponseCode.COMMON_FAIL.getMessage(); this.data = data; } public ResponseBody(boolean success, ResponseCode resultEnum, T data) { this.success = success; this.statusCode = success ? ResponseCode.SUCCESS.getCode() : (resultEnum == null ? ResponseCode.COMMON_FAIL.getCode() : resultEnum.getCode()); this.msg = success ? ResponseCode.SUCCESS.getMessage() : (resultEnum == null ? ResponseCode.COMMON_FAIL.getMessage() : resultEnum.getMessage()); this.data = data; } }
package com.security.common; public enum ResponseCode { SUCCESS(200, "成功"), COMMON_FAIL(999, "失败"), PARAM_NOT_VALID(1001, "参数无效"), PARAM_IS_BLANK(1002, "参数为空"), PARAM_TYPE_ERROR(1003, "参数类型错误"), PARAM_NOT_COMPLETE(1004, "参数缺失"), USER_NOT_LOGIN(2001, "用户未登录"), USER_ACCOUNT_EXPIRED(2002, "账号已过期"), USER_CREDENTIALS_ERROR(2003, "密码错误"), USER_CREDENTIALS_EXPIRED(2004, "密码过期"), USER_ACCOUNT_DISABLE(2005, "账号不可用"), USER_ACCOUNT_LOCKED(2006, "账号被锁定"), USER_ACCOUNT_NOT_EXIST(2007, "账号不存在"), USER_ACCOUNT_ALREADY_EXIST(2008, "账号已存在"), USER_ACCOUNT_USE_BY_OTHERS(2009, "账号多点登录,账号下线"), USER_SESSION_INVALID(2010,"登录超时"), NO_PERMISSION(3001, "没有权限"); private Integer code; private String message; ResponseCode(Integer code, String message){ this.code = code; this.message = message; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public static String getMessageByCode(Integer code) { for (ResponseCode ele : values()) { if (ele.getCode().equals(code)) { return ele.getMessage(); } } return null; } }WebSecurityConfigurerAdapter
package com.security.config; import com.security.common.CaptchaFilter; import com.security.common.JwtAuthenticationFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.ObjectPostProcessor; 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.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Autowired private UserDetailsService userDetailsService; @Autowired private AjaxAuthenticationSuccessHandler ajaxAuthenticationSuccessHandler; @Autowired private AjaxAuthenticationFailureHandler ajaxAuthenticationFailureHandler; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //自定义数据库登录逻辑 auth.userDetailsService(userDetailsService) //设置加密方式 .passwordEncoder(passwordEncoder()); } }
至此,我们已经可以完成一个非常简单的认证。
注意:在配置完成 Spring Security 之后, Spring 会赠送 “/login”以及 "/logout"接口,无需自己实现。
但是,我们实际应用中不可能每次访问资源都重新登录,而且,前文中我们明确拒绝使用 cookie,那用什么方式实现身份验证呢?答案是 JWT。
JWT 过滤器在这一步中,我们需要通过继承 BasicAuthenticationFilter 类并重写 doFilterInternal 方法来实现 JWT 解析、身份验证和自动登录。
要做到这一步,前端发送的所有请求的请求头必须带有 JWT,整体的流程是在第一次登录成功后,将 JWT 写入响应头,前端接收到之后将其存储,并在之后的每一次请求中,都将 JWT 写入请求头之中。并且由于之前登录时已经将权限信息写入缓存,所以在校验JWT 通过之后,应该先从缓存中取出权限,若缓存中没有权限才重新查询数据库。
package com.security.common; import cn.hutool.core.util.StrUtil; import com.security.mapper.APIMapper; import com.security.mapper.RoleMapper; import com.security.utils.JwtUtils; import com.security.utils.RedisUtils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; 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.ArrayList; import java.util.Arrays; import java.util.List; public class JwtAuthenticationFilter extends BasicAuthenticationFilter { @Autowired private JwtUtils jwtUtils; @Autowired private APIMapper apiMapper; @Autowired private RoleMapper roleMapper; @Autowired private RedisUtils redisUtils; public JwtAuthenticationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { //获取jwt String jwt = request.getHeader(jwtUtils.getHeader()); if (StrUtil.isBlankOrUndefined(jwt)){ //如果jwt为空,则过滤链继续往下执行 chain.doFilter(request,response); return; } Claims claims = jwtUtils.parseToken(jwt); //判断jwt是否被篡改、是否解析异常 if (claims == null){ throw new JwtException("token 异常"); } //判断jwt是否过期 if (jwtUtils.isTokenExpired(claims)){ throw new JwtException("token 过期"); } String username = claims.getSubject(); ListcodeList = null; if (redisUtils.hashKey(username)){ String value = redisUtils.get(username).toString(); codeList = Arrays.asList(value.split(",")); }else { //用户、角色、资源方案:使用角色控制权限 codeList = roleMapper.queryUserRole(username); //用户、角色、权限、资源方案:使用“权限”控制权限 //codeList = authoritiesMapper.queryAuthoritiesList(user.getUsername()); } List authorities = new ArrayList<>(); codeList.forEach(code ->{ SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(code); authorities.add(simpleGrantedAuthority); }); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities); //根据上下文,获取用户的权限,实现自动登录 SecurityContextHolder.getContext().setAuthentication(token); chain.doFilter(request,response); } }
如果,认证在稍微复杂一点,加上验证码认证呢?
验证码验证码的整体工作流程是:当前端调用验证码的 API 时,后端在生成随机字符 code 的同时,生成一个随机码 key,然后将随机码和验证码写入缓存,并将随机码和根据验证码生成的图片验证码返回给前端。前端登陆时,不仅返回用户名、密码、验证码,还需要将随机码 key 一同返回,后端收到随机码 key 之后,就可以从缓存中取出随机码 code,之后只需要将缓存中的 code 与用户输入的 code 进行比对即可。
@GetMapping("/getCaptcha") public ResponseBody getCode() throws IOException { //生成随机码,作为验证码的key值,传给前端(方便验证时,根据key从redis中取出正确的验证码value) String key = UUID.randomUUID().toString(); // 随机生成宽200、高100的 4 位验证码 ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(200, 100, 4, 4); String code = captcha.getCode(); System.out.println("key:"+key); System.out.println(code); //写入到流中 BufferedImage bufferedImage = captcha.getImage(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ImageIO.write(bufferedImage,"jpg", outputStream); //进行base64编码 base64Encoder base64Encoder = new base64Encoder(); //编码前缀 String str = "data:image/jpeg;base64,"; //使用hutool自己提供的方法,直接获取base64编码 //String base64 = str + captcha.getImagebase64(); String base64Image = str + base64Encoder.encode(outputStream.toByteArray()); //将验证码和对应的随机key值写入缓存数据库 redisUtils.set(key,code,600); return ResponseBodyUtil.success( MapUtil.builder() .put("key",key) .put("base64Image", base64Image) .build() );
由于Spring Security 本身并没有自带验证码过滤器,所以,我们可以通过继承 OncePerRequestFilter抽象类实现验证码过滤器 ,并且将该过滤器设置在用户名、密码、权限过滤器之前。这样每次访问接口都会经过此过滤器,我们可以获取请求路径,并判定当请求路径为/login时进入验证码验证流程。
package com.security.common; import com.security.config.AjaxAuthenticationFailureHandler; import com.security.exception.CaptchaException; import com.security.utils.RedisUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class CaptchaFilter extends OncePerRequestFilter { @Autowired private RedisUtils redisUtils; @Autowired private AjaxAuthenticationFailureHandler ajaxAuthenticationFailureHandler; @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { String url = httpServletRequest.getRequestURI(); //当url为登录路径且请求方式为post时,进入该过滤器处理 if ("/login".equals(url) && "POST".equals(httpServletRequest.getMethod())){ try { validate(httpServletRequest); }catch (CaptchaException captchaException){ //如果不正确,扑获到验证码异常就交给认证失败处理器 ajaxAuthenticationFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,captchaException); } } //验证成功,则过滤链继续往下执行 filterChain.doFilter(httpServletRequest,httpServletResponse); } //验证码校验逻辑 private void validate(HttpServletRequest httpServletRequest) { String code = httpServletRequest.getParameter("code"); String key = httpServletRequest.getParameter("key"); //判断是否为空 if (StringUtils.isBlank(code) || StringUtils.isBlank(key)){ throw new CaptchaException("验证码错误"); } if (!code.equals(redisUtils.get(key))){ throw new CaptchaException("验证码错误"); } //删除缓存,一次性使用 redisUtils.del(key); } }
由于上一步抛出了验证码异常,所以,我们需要实现该异常处理
package com.security.exception; import org.springframework.security.core.AuthenticationException; public class CaptchaException extends AuthenticationException { public CaptchaException(String msg) { super(msg); } }修改 WebSecurityConfigurerAdapter
添加 JWT 与验证码过滤器之后的 Security 配置类:
package com.security.config; import com.security.common.CaptchaFilter; import com.security.common.JwtAuthenticationFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.ObjectPostProcessor; 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.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Autowired private UserDetailsService userDetailsService; @Autowired private AjaxAuthenticationSuccessHandler ajaxAuthenticationSuccessHandler; @Autowired private AjaxAuthenticationFailureHandler ajaxAuthenticationFailureHandler; @Autowired private CaptchaFilter captchaFilter; @Bean JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception { JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager()); return jwtAuthenticationFilter; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //自定义数据库登录逻辑 auth.userDetailsService(userDetailsService) //设置加密方式 .passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { //添加自定义过滤器,并设置在用户名密码权限过滤器之前 http.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class) .addFilter(jwtAuthenticationFilter()); //授权 http.authorizeRequests() //放行不需要拦截的请求 .antMatchers("/login","/getCaptcha").permitAll() //所有请求必须认证才能访问 .anyRequest().authenticated() //注销 .and() .logout().permitAll() //注销后,删除cookie .deletecookies("JSESSIONID") .and() //设置登录方式为表单提交 .formLogin().permitAll() //前后端分离:登录成功处理器,前端通过json数据进行页面跳转 .successHandler(ajaxAuthenticationSuccessHandler) .failureHandler(ajaxAuthenticationFailureHandler) ; //关闭CSRF跨域 http.csrf().disable(); } }
认证通过后,用户的权限信息会封装成一个User(此 User 非彼 User,而是 Spring Security 的 USer)放到 Spring 的全局缓存 SecurityContextHolder 中,以备后面访问资源时使用。
授权管理在授权管理中,用户可以访问什么资源(API)取决于用户具有什么角色/权限。授权的流程如图所示:
我们首先需要继承 AbstractSecurityInterceptor 资源管理拦截器抽象类,并实现 servler 的 Filter 接口,从而实现过滤 URL 并拦截请求。
package com.security.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.SecuritymetadataSource; import org.springframework.security.access.intercept.AbstractSecurityInterceptor; import org.springframework.security.access.intercept.InterceptorStatusToken; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecuritymetadataSource; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import javax.servlet.*; import java.io.IOException; @Component public class UrlAbstractSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { @Autowired private FilterInvocationSecuritymetadataSource securitymetadataSource; @Autowired public void setUrlAccessDecisionManager(UrlAccessDecisionManager urlAccessDecisionManager) { super.setAccessDecisionManager(urlAccessDecisionManager); } @Override public Class> getSecureObjectClass() { return FilterInvocation.class; } @Override public SecuritymetadataSource obtainSecuritymetadataSource() { return this.securitymetadataSource; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain); invoke(fi); } private void invoke(FilterInvocation fi) throws IOException, ServletException { //FilterInvocation里面有一个被拦截的url //里面调用InvocationSecuritymetadataSource实现类的getAttributes(Object object)这个方法获取fi对应的所有权限 //再调用AccessDecisionManager实现类的decide方法来校验用户的权限是否足够 InterceptorStatusToken token = super.beforeInvocation(fi); try { //执行下一个拦截器 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.afterInvocation(token, null); } } }FilterInvocationSecuritymetadataSource 实现类
该类的主要功能就是通过当前的请求地址获取该地址需要的用户角色,并将访问该URL 所需要的角色权限信息传给决策器,由决策器进行表决。
package com.security.config; import com.security.mapper.APIMapper; import com.security.mapper.AuthoritiesMapper; import com.security.mapper.RoleAPIMapper; import com.security.mapper.RoleMapper; import com.security.pojo.API; import com.security.pojo.Authorities; import com.security.pojo.Role; import com.security.pojo.RoleAPI; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecuritymetadataSource; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.Collection; import java.util.linkedList; import java.util.List; @Component public class UrlFilterInvocationSecuritymetadataSource implements FilterInvocationSecuritymetadataSource { @Autowired private RoleMapper roleMapper; @Autowired private AuthoritiesMapper authoritiesMapper; @Override public CollectiongetAttributes(Object object) throws IllegalArgumentException { //获取请求地址 String requestUrl = ((FilterInvocation) object).getRequestUrl(); //用户、角色、资源方案:查询具体某个接口的权限 List roleList = roleMapper.selectListByUrl(requestUrl); //用户、角色、权限、资源方案:查询某个具体的权限 //List authoritiesList = authoritiesMapper.queryAuthoritiesByUrl(requestUrl); if(roleList == null || roleList.size() == 0){ //请求路径没有配置权限,表明该请求接口可以任意访问 return null; } String[] attributes = new String[roleList.size()]; for(int i = 0;i getAllConfigAttributes() { return null; } @Override public boolean supports(Class> aClass) { return true; } }
在这个实现类中,getAttributes(Object o) 方法返回的集合最终会来到 AccessDecisionManager 类。
AccessDecisionManager 实现类授权管理器会通过spring的全局缓存 SecurityContextHolder 获取用户的权限信息,还会获取被拦截的 URL 和被拦截 URL 所需的全部权限,然后根据所配的策略进行判定。我们需要重写授权管理器的 decide() 方法,对访问的 URL 进行权限认证处理。decide() 方法接收的三个参数,第一个参数保存了当前登录用户的角色信息,第二个参数是请求的 URL,第三个参数是 getAttributes() 方法返回的权限集合。而当前请求所需的权限和当前用户具有的权限有一个符合即可正常访问。
package com.security.config; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; import java.util.Collection; import java.util.Iterator; @Component public class UrlAccessDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication, Object o, Collectioncollection) throws AccessDeniedException, InsufficientAuthenticationException { Iterator iterator = collection.iterator(); while (iterator.hasNext()) { ConfigAttribute ca = iterator.next(); //当前请求需要的权限 String needRole = ca.getAttribute(); //当前用户所具有的权限 Collection extends GrantedAuthority> authorities = authentication.getAuthorities(); for (GrantedAuthority authority : authorities) { //只要符合一个即可访问 if (authority.getAuthority().equals(needRole)) { return; } } } throw new AccessDeniedException("权限不足!"); } @Override public boolean supports(ConfigAttribute configAttribute) { return true; } @Override public boolean supports(Class> aClass) { return true; } }
由于我们上一步在用户权限与请求权限不相符合时抛出了AccessDeniedException异常,所以,我们需要自定义实现该异常。
权限不足处理器package com.security.config; import com.security.common.ResponseBody; import com.security.common.ResponseCode; import com.security.utils.ResponseBodyUtil; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class AjaxAccessDeniedHandler extends JSONAuthentication implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { ResponseBody responseBody = ResponseBodyUtil.fail(ResponseCode.NO_PERMISSION); this.WriteJSON(httpServletRequest,httpServletResponse,responseBody); } }匿名访问处理器
为了处理匿名访问 API,自定义实现匿名访问处理器
package com.security.config; import com.security.common.ResponseBody; import com.security.common.ResponseCode; import com.security.utils.ResponseBodyUtil; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class AjaxAuthenticationEntryPoint extends JSONAuthentication implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { ResponseBody responseBody = ResponseBodyUtil.fail(ResponseCode.USER_NOT_LOGIN); this.WriteJSON(httpServletRequest,httpServletResponse,responseBody); } }修改 WebSecurityConfigurerAdapter
正常的业务还需要添加会话过期处理器、多点登录处理器、注销成功/失败处理器等,本文不做一一阐述,添加完所有处理器后的完整的 WebSecurityConfigurerAdapter 如下:
package com.security.config; import com.security.common.CaptchaFilter; import com.security.common.JwtAuthenticationFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.ObjectPostProcessor; 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.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Autowired private UserDetailsService userDetailsService; @Autowired private AjaxAuthenticationSuccessHandler ajaxAuthenticationSuccessHandler; @Autowired private AjaxAuthenticationFailureHandler ajaxAuthenticationFailureHandler; @Autowired private AjaxAuthenticationEntryPoint ajaxAuthenticationEntryPoint; @Autowired private AjaxAccessDeniedHandler ajaxAccessDeniedHandler; @Autowired private AjaxLogoutHandler ajaxLogoutHandler; @Autowired private AjaxLogoutSuccessHandler ajaxLogoutSuccessHandler; @Autowired private AjaxInvalidSessionStrategy ajaxInvalidSessionStrategy; @Autowired private AjaxSessionInformationExpiredStrategy ajaxSessionInformationExpiredStrategy; @Autowired private CaptchaFilter captchaFilter; @Autowired private UrlFilterInvocationSecuritymetadataSource urlFilterInvocationSecuritymetadataSource; @Autowired private UrlAccessDecisionManager urlAccessDecisionManager; @Autowired private UrlAbstractSecurityInterceptor urlAbstractSecurityInterceptor; @Bean JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception { JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager()); return jwtAuthenticationFilter; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //自定义数据库登录逻辑 auth.userDetailsService(userDetailsService) //设置加密方式 .passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { //添加自定义过滤器,并设置在用户名密码权限过滤器之前 http.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class) .addFilter(jwtAuthenticationFilter()) .addFilterBefore(urlAbstractSecurityInterceptor, FilterSecurityInterceptor.class); //授权 http.authorizeRequests() .withObjectPostProcessor(new ObjectPostProcessor() { @Override public O postProcess(O o) { //决策管理器 o.setAccessDecisionManager(urlAccessDecisionManager); //安全元数据源 o.setSecuritymetadataSource(urlFilterInvocationSecuritymetadataSource); return o; } }) //放行不需要拦截的请求 .antMatchers("/login","/getCaptcha").permitAll() //所有请求必须认证才能访问 .anyRequest().authenticated() //注销 .and() .logout().permitAll() .addLogoutHandler(ajaxLogoutHandler) //注销成功 .logoutSuccessHandler(ajaxLogoutSuccessHandler) //注销后,删除cookie .deletecookies("JSESSIONID") .and() //设置登录方式为表单提交 .formLogin().permitAll() .successHandler(ajaxAuthenticationSuccessHandler) .failureHandler(ajaxAuthenticationFailureHandler) .and() .exceptionHandling() //权限不足处理 .accessDeniedHandler(ajaxAccessDeniedHandler) //未登录,访问资源的异常 .authenticationEntryPoint(ajaxAuthenticationEntryPoint) //会话管理 .and() .sessionManagement() //会话过期策略 .invalidSessionStrategy(ajaxInvalidSessionStrategy) //最大允许登录数 .maximumSessions(1) //达到最大登录数后,是否允许继续登录(否会挤掉已经登录的账户) .maxSessionsPreventsLogin(false) //多点登录处理方式 .expiredSessionStrategy(ajaxSessionInformationExpiredStrategy) ; //关闭CSRF跨域 http.csrf().disable(); //前后端分离是无状态的,所以使用STATELESS策略 http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } }
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)