SpringSecurity为我们提供了基于注解的权限控制方案,实现限制访问资源所需权限。
首先开启权限,在 SecurityConfig 配置类上添加:
// 开启权限配置
@EnableGlobalMethodSecurity(prePostEnabled = true)
修改UserDetails的实现类LoginUser,让其能封装权限信息
package org.example.bean;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* 因为UserDetailsService方法的返回值是UserDetails类型,
* 所以需要定义一个类,实现该接口,把用户信息封在其中。1
*/
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
/**
* permissions 存放权限集合
*/
private List<String> permissions;
/**
* @JSONField(serialize = false) 不序列化
* getAuthorities方法的返回值
*/
@JSONField(serialize = false)
private List<SimpleGrantedAuthority> authorities;
public LoginUser(User user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null){
return authorities;
}
//把permissions中String类型的权限信息封装成它的实现类SimpleGrantedAuthority对象
/*authorities = new ArrayList<>();
for (String permission : permissions) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
newList.add(authority);
}
return newList;*/
// 使用stream流操作
authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return authorities;
}
@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;
}
}
再 UserDetailsServiceImpl 中把权限信息封装到LoginUser中,这里先写死,模拟权限。
package org.example.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.example.bean.LoginUser;
import org.example.bean.User;
import org.example.mapper.UserMapper;
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.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;
/**
* 创建一个类实现UserDetailsService接口,加载用户特定数据的核心接口。
* 重写其中的方法。更加用户名从数据库中查询用户信息
* 里面定义了一个根据用户名查询用户信息的方法
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(queryWrapper);
//如果没有查询到用户,就抛出异常
if(Objects.isNull(user)){
throw new RuntimeException("用户名或者密码错误");
}
//把数据封装成userDetails返回,ArrayList为权限集合,此处作为模拟添加
return new LoginUser(user,new ArrayList<>(Arrays.asList("test","admin")));
}
}
在执行请求前,实现了 OncePerRequestFilter 接口的 JwtAuthenticationTokenFilter 过滤器里添加权限集合
package org.example.filter;
import io.jsonwebtoken.Claims;
import org.example.SecurityQuickstartApplication;
import org.example.bean.LoginUser;
import org.example.util.JwtUtil;
import org.example.util.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
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;
import java.util.Objects;
/**
* OncePerRequestFilter
* spirng自带的过滤器,可以保证一次请求只经过一次这个过滤器
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = httpServletRequest.getHeader("token");
// 如果请求头不存在token,则放行并返回
if(!StringUtils.hasText(token)){
// 放行后,会去执行后面的filter链,执行完 filter链后,返回到放行这里,会继续往下执行,所以return;
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}
// 解析token
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
// getSubject,此方法获取userId,即数据库表中的主键id
userId = claims.getSubject();
} catch (Exception e) {
throw new RuntimeException("解析token失败!");
}
// 从redis获取用户信息
// 拼接reidsKey,在LoginServiceImpl登录时,将完整的用户信息存入Redis,login:+userId作为key
String redisKey = "login:"+userId;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录!");
}
// 存入SecurityContextHolder,将 loginUser.getAuthorities()中的权限集合放到 authenticationToken 中
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
最后在 controller 中添加 @PreAuthorize("hasAuthority('abc')") 注解,其中 abc、test 为权限名,即在 UserDetailsServiceImpl 默认写死的权限集合中的一个
/**
* 测试没有abc权限是否可以登录
* @return
*/
@GetMapping("/getTest2")
@PreAuthorize("hasAuthority('abc')")
public String getTest2(){
logger.info("测试没有abc权限是否可以登录");
return "chenggong";
}
/**
* 测试没有test权限是否可以登录
* @return
*/
@GetMapping("/getTest3")
@PreAuthorize("hasAuthority('test')")
public String getTest3(){
logger.info("测试有test权限是否可以登录");
return "chenggong";
}
测试:getTest2
getTest3