[title]前言
Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。
[/title]
一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
而认证和授权也是SpringSecurity作为安全框架的核心功能。
java引入(maven方式)
org.springframework.boot
spring-boot-starter-security
知识要点:
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
ExceptionTranslationFilter: 处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
FilterSecurityInterceptor: 负责权限校验的过滤器。
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保存在SecurityContextHolder中。SecurityContextHolder默认使用ThreadLocal 策略来存储认证信息。
获取当前用户的信息
因为身份信息是与线程绑定的,所以可以在程序的任何地方使用静态方法获取用户信息。一个典型的获取当前登录用户的姓名的例子如下所示:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
getAuthentication()返回了认证信息,再次getPrincipal()返回了身份信息,UserDetails便是Spring对身份信息封装的一个接口。
创建一个类实现UserDetailsService接口,重写其中的方法。更加用户名从数据库中查询用户信息
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询用户信息
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(wrapper);
//如果查询不到数据就通过抛出异常来给出提示
if(Objects.isNull(user)){
throw new RuntimeException("用户名或密码错误");
}
//TODO 根据用户查询权限信息 添加到LoginUser中
//封装成UserDetails对象返回
return new LoginUser(user);
}
}
因为UserDetailsService方法的返回值是UserDetails类型,所以需要定义一个类,实现该接口,把用户信息封装在其中。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private User user;
//获取权限信息
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
这个LoginUser需要实现UserDetails 重写其中的方法,并且把自己的User对象封装进去
登录逻辑
//Authentication是接口的实现类,封装用户密码返回Authentication类型的参数 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword()); //authenticationManager接口调用authenticate方法,方法返回的是Authentication对象,需要Authentication类型的参数。 //Authentication是接口,使用这个接口的实现类来把用户的账号密码封装成这个类型的参数。供authenticationManager接口的authenticate方法调用 //authenticationManager.authenticate 用户校验 查数据库 Authentication authenticate = authenticationManager.authenticate(authenticationToken); 然后强转成 LoginUser (authenticate 内就已经封装了 LoginUser的信息) [qzdypre]
//如果查询不到用户,就抛出异常 if(Objects.isNull(authenticate)){ throw new RuntimeException("用户名或密码错误"); } //把authenticate对象强转为LoginUser对象 LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); //取出LoginUser 里的用户id String userId = loginUser.getUser().getId().toString(); //使用userid生成token String jwt = JwtUtil.createJWT(userId); System.out.println(jwt); //authenticate存入redis,如: "login:1"为key,LoginUser对象为value redisUtil.set("login:"+userId,loginUser); //把token响应给前端 HashMapmap = new HashMap<>(); map.put("token",jwt); return new ResponseResult(200,"登陆成功",map);
[/qzdypre] token的拦截器 filter
Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private RedisUtil redisUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取前端请求头的token
String token = request.getHeader("token");
//没有token要放行,不然登录页面都进不了
if (!StringUtils.hasText(token)) {
//放行
filterChain.doFilter(request, response);
return;//直接return,不走下面解析token的代码了
}
//解析token
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
//解析token获取userid
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis中获取用户信息 (key就是解析出的token的userid)
String redisKey = "login:" + userid;
LoginUser loginUser = (LoginUser) redisUtil.get(redisKey);
//如果注销了,Redis数据就被删除,此时抛出异常用户未登录!
if(Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
//存入SecurityContextHolder,
// TODO 每次请求都存一次????
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//SecurityContextHolder.getContext().getAuthentication()
//放行
filterChain.doFilter(request, response);
}
}
我们要封装权限信息 需要把
Listpermissions 封装到 spring security 提供的权限对象里
GrantedAuthority 最简单的方法:
//存储SpringSecurity所需要的权限信息的集合 @JSONField(serialize = false) private Listauthorities;
for (String permission:permissions) { SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission); authorities.add(authority); } 直接遍历 permissions这个String类型的list 遍历的结果add到 存储权限的新集合中 但是可以使用 stream流快速转换 @JSONField(serialize = false) private Listauthorities;
//把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中 authorities = permissions.stream(). map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); return authorities;
springSecurity会抛出两大类异常,一个是AuthenticationException(认证的异常),一个是AccessDeniedException(权限不足的异常)
我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint 对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "权限不足");
String json = JSON.toJSONString(result);
WebUtils.renderString(response,json);
}
}
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录");
String json = JSON.toJSONString(result);
WebUtils.renderString(response,json);
}
}
跨域
@Configuration
public class CorsConfig implements WebMvcConfigurer {
//跨域
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 匹配所有的路径
.allowCredentials(true) // 设置允许凭证
.allowedHeaders("*") // 设置请求头
.allowedMethods("GET", "POST", "DELETE", "PUT");
}
}
在spring security配置类里配置允许跨域
//允许跨域 http.cors();