Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
Spring Security对Web安全性的支持大量地依赖于Servlet过滤器。这些过滤器拦截进入请求,并且在应用程序处理该请求之前进行某些安全处理。 Spring Security提供有若干个过滤器,它们能够拦截Servlet请求,并将这些请求转给认证和访问决策管理器处理,从而增强安全性。根据自己的需要,可以使用适当的过滤器来保护自己的应用程序。
本文权限认证不采用springsecurity的注解方式,使用自定义的权限认证,这样不需要在控制层的每个接口前面加上权限注解,具体请看下面的权限校验流程。
具体代码查看实现中的github地址
首先,系统会将输入的用户名和密码放入authentication中,之后进入UserDetailsService的实现类中,调用sql,通过用户名查询账号信息和角色信息,统一存储在UserDetails中,之后PasswordEncoder会将查询到的账号密码和authentication中密码进行比对,密码不一致,则抛出异常(认证失败),密码正确则开始执行登录的业务,使用jwt生成token,将UserDetails存储到redis中,然后将token和角色id集返回给客户端。

首先,进入自定义的过滤器中,获取客户端传的token,如果token为空,则进入下一层跳出该过滤器,进入配置中,查询是否为不需要权限认证的接口,不是则抛出异常(认证失败);如果token不为空,则使用jwt解析token,获取其中的userId,根据该userId查询redis中存储的用户信息,查询为空则抛出异常(账户过期,请重新登录),之后获取客户端传的role_id(角色id,因为一个用户可能有多个角色,因此需要传个角色id,告诉服务器当前角色),将客户端传的role_id和之前在redis中查询的用户信息比对,查看是否存在该角色,存在,则通过该role_id查询redis中该角色的权限,判断当前请求路径该角色是否拥有权限,有权限则将权限信息封装到authentication中,放行。

具体代码请看github地址
https://github.com/cn-g/springsecurity
下面展示关键代码实现
因为新版本SpringSecurity弃用了WebSecurityConfigurerAdapter,所以新版的SpringSecurity需要更换SpringSecurity的配置文件,下面将新版和老版的springsecurity配置代码各自展示了一份
不继承WebSecurityConfigurerAdapter
import com.example.springsecurity.filter.JwtAuthenticationTokenFilter;
import com.example.springsecurity.filter.AuthenticationEntryPointImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.authentication.configuration.GlobalAuthenticationConfigurerAdapter;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;/*** @author Admin*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig{@ResourceJwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@ResourceAuthenticationEntryPointImpl authenticationEntryPoint;@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeHttpRequests()//登录接口,允许所有人访问.antMatchers("/user/login").permitAll()//除了上面的接口,其它接口都需要鉴权认证.anyRequest().authenticated();//配置登入认证失败、权限认证失败异常处理器http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);//把token校验过滤器添加到过滤链中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//允许跨域http.cors();return http.build();}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {final List configurers = new ArrayList<>();configurers.add(new GlobalAuthenticationConfigurerAdapter() {@Overridepublic void configure(AuthenticationManagerBuilder auth){// auth.doSomething()}});return authConfig.getAuthenticationManager();}}
继承WebSecurityConfigurerAdapter
import com.example.springsecurity.filter.JwtAuthenticationTokenFilter;
import com.example.springsecurity.filter.AuthenticationEntryPointImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.authentication.configuration.GlobalAuthenticationConfigurerAdapter;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;/*** @author xu*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{@ResourceJwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@ResourceAuthenticationEntryPointImpl authenticationEntryPoint;@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception{http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeHttpRequests()//登录接口,允许所有人访问.antMatchers("/user/login").permitAll()//除了上面的接口,其它接口都需要鉴权认证.anyRequest().authenticated();//配置登入认证失败、权限认证失败异常处理器http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);//把token校验过滤器添加到过滤链中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//允许跨域http.cors();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception{return supper.authenticationManagerBean();}}
/*** token校验以及权限校验* * @author xu*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {private Logger logger = LoggerFactory.getLogger(getClass());@Autowiredprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {// 获取tokenString token = request.getHeader("token");if (!StringUtils.hasText(token)) {// 放行filterChain.doFilter(request, response);return;}String userId;// 解析tokentry {Claims claims = JwtUtil.parseJWT(token);userId = claims.getSubject();} catch (Exception e) {logger.error("解析token失败");WebUtil.renderString(response, JSON.toJSONString(ResponseModels.loginException()));return;}// 从redis中获取用户信息String redisKey = "login:" + userId;LoginUser loginUser = redisCache.getCacheObject(redisKey);if (ObjectUtils.isEmpty(loginUser)) {logger.error("用户信息获取失败");WebUtil.renderString(response, JSON.toJSONString(ResponseModels.commonException("账户过期,请重新登录")));return;}String roleId = request.getHeader("role_id");if(ObjectUtils.isEmpty(roleId)){logger.error("无角色id");WebUtil.renderString(response, JSON.toJSONString(ResponseModels.loginException()));return;}// 校验是否有该角色if (!loginUser.getPermissions().contains(roleId)) {logger.error("角色不匹配");WebUtil.renderString(response, JSON.toJSONString(ResponseModels.noPowerException()));return;}String url = request.getRequestURI();String role = redisCache.getCacheObject("role:role_" + roleId);// 校验角色是否存在该路径if (!role.contains(url)) {logger.error("该接口路径无权限");WebUtil.renderString(response, JSON.toJSONString(ResponseModels.noPowerException()));return;}// 获取权限信息封装到Authentication中UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());// 存入securityContextHolderSecurityContextHolder.getContext().setAuthentication(authenticationToken);// 放行filterChain.doFilter(request, response);}
/*** UserDetailsService实现类* * @author xu*/
@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate RoleMapper roleMapper;@Overridepublic UserDetails loadUserByUsername(String userName) {log.info("用户名称为:{}", userName);User user = userMapper.selectOne(Wrappers.lambdaQuery(User.class).eq(User::getUsername, userName).eq(User::getStatus, 0));if (ObjectUtils.isEmpty(user)) {throw new UsernameNotFoundException("用户名不存在");}// 获取当前用户角色信息List list = roleMapper.selectRoleByUserId(user.getId());log.info("用户的角色为:{}", list);//LoginUser为UserDetails的实现类return new LoginUser(user, list);}
}
public ResponseModelDto login(User user) {UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());Authentication authentication = authenticationManager.authenticate(authenticationToken);if (ObjectUtils.isEmpty(authentication)) {throw new CommonException("用户名或密码错误");}log.info("用户登录成功:{}", authentication);// 使用userId生成tokenLoginUser loginUser = (LoginUser)authentication.getPrincipal();String userId = loginUser.getUser().getId().toString();String jwt = JwtUtil.createJWT(userId);redisCache.setCacheObject("login:" + userId, loginUser);// 获取当前用户角色信息List list = roleMapper.selectRoleByUserId(Long.valueOf(userId));HashMap map = new HashMap<>();map.put("token", jwt);map.put("role", String.join(",",list));//返回token和角色id集return ResponseModels.ok(map);}@Overridepublic ResponseModelDto logout() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser = (LoginUser)authentication.getPrincipal();Long userId = loginUser.getUser().getId();redisCache.deleteObject("login:" + userId);return ResponseModels.ok("登出成功");}