介绍
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配置文件
因为新版本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{
@Resource
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Resource
AuthenticationEntryPointImpl authenticationEntryPoint;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public 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();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
final List<GlobalAuthenticationConfigurerAdapter> configurers = new ArrayList<>();
configurers.add(new GlobalAuthenticationConfigurerAdapter() {
@Override
public 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{
@Resource
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Resource
AuthenticationEntryPointImpl authenticationEntryPoint;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected 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
@Override
public AuthenticationManager authenticationManagerBean() throws Exception{
return supper.authenticationManagerBean();
}
}
过滤器实现
/**
* token校验以及权限校验
*
* @author xu
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
// 放行
filterChain.doFilter(request, response);
return;
}
String userId;
// 解析token
try {
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());
// 存入securityContextHolder
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(request, response);
}
UserDetailsServiceImpl实现:
/**
* UserDetailsService实现类
*
* @author xu
*/
@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Override
public 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<String> 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生成token
LoginUser loginUser = (LoginUser)authentication.getPrincipal();
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);
redisCache.setCacheObject("login:" + userId, loginUser);
// 获取当前用户角色信息
List<String> list = roleMapper.selectRoleByUserId(Long.valueOf(userId));
HashMap<String, String> map = new HashMap<>();
map.put("token", jwt);
map.put("role", String.join(",",list));
//返回token和角色id集
return ResponseModels.ok(map);
}
@Override
public ResponseModelDto logout() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser)authentication.getPrincipal();
Long userId = loginUser.getUser().getId();
redisCache.deleteObject("login:" + userId);
return ResponseModels.ok("登出成功");
}