SpringSecurity
SpringSecurity简介
ref:
Spring Security,这是一种基于 Spring AOP
和 Servlet
过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。本教程对 Spring Security 的使用进行一个比较全面的简要介绍。(摘自w3cschool)
创建工程与引入依赖
创建springboot项目或空maven项目
parent
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
dependencys
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping
public String hello(){
return "hello , spring security!";
}
}
启动项目访问/hello,会跳转到/login,这是默认的
流程
登录校验流程
- UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
- ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。
- FilterSecurityInterceptor:负责权限校验的过滤器。
登录拦截思路
登录
①自定义登录接口
调用ProviderManager的方法进行认证如果认证通过生成jwt
把用户信息存入redis中
②自定义UserDetailsService
在这个实现类中去查询数据库
校验:
①定义Jwt认证过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContextHolder
引入redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
工具类与配置类
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisCacheAutoConfiguration {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用fastJson
template.setValueSerializer(fastJsonRedisSerializer);
// hash的value序列化方式采用fastJson
template.setHashValueSerializer(fastJsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
static {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}
private final Class<T> clazz;
public FastJsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
/**
* 序列化
*/
@Override
public byte[] serialize(T t) throws SerializationException {
if (null == t) {
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
/**
* 反序列化
*/
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (null == bytes || bytes.length <= 0) {
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return (T) JSON.parseObject(str, clazz);
}
}
public class JwtUtil {
// 默认 生成之后有效时长 1小时
private static final Long EXPIRE= 60 * 60 * 1000L;
// 密钥明文
private static final String JWT_KEY= "securityKey";// 不能有_
public static String getUUID(){
return UUID.randomUUID().toString().replaceAll("-", "");
}
/**
* 生成jwt
* @param subject token存放的数据 json格式
* @return String
*/
public static String createJwt(String subject){
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
return builder.compact();
}
/**
* 生成jwt
* @param subject token存放的数据 json格式
* @param expire 有效时间
* @return String
*/
public static String createJwt(String subject, Long expire){
JwtBuilder builder = getJwtBuilder(subject, expire, getUUID());
return builder.compact();
}
/**
* 生成jwt
* @param subject token存放的数据 json格式
* @param expire 有效时间
* @param id id
* @return String
*/
public static String createJwt(String subject, Long expire, String id){
JwtBuilder builder = getJwtBuilder(subject, expire, id);
return builder.compact();
}
/**
*
* @param subject subject
* @param expire 过期时长 null 时 默认使用EXPIRATION,1小时
* @param uuid uuid
* @return JwtBuilder
*/
private static JwtBuilder getJwtBuilder(String subject, Long expire, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if (expire == null){
expire = JwtUtil.EXPIRE;
}
long expMillis = nowMillis + expire;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) // 唯一id
.setSubject(subject) // 主题,可以是json
.setIssuer("ctp") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) // 使用HS256算法签名,第二个时密钥
.setExpiration(expDate); // 过期时间
}
/**
* 生成加密密钥
* @return SecretKey
*/
public static SecretKey generalKey(){
byte[] decodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKeySpec key = new SecretKeySpec(decodeKey, 0, decodeKey.length, "AES");
return key;
}
/**
* 解析jwt
* @param jwt jwt
* @return Claims
*/
public static Claims parseJwt(String jwt){
SecretKey secretKey = generalKey();
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
@Component
public class RedisUtil {
@Resource
private RedisTemplate<Object, Object> redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @return 缓存的对象
*/
public ValueOperations<Object, Object> setCacheObject(Object key, Object value) {
ValueOperations<Object, Object> operation = redisTemplate.opsForValue();
operation.set(key, value);
return operation;
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
* @return 缓存的对象
*/
public ValueOperations<Object, Object> setCacheObject(Object key, Object value, Integer timeout, TimeUnit timeUnit) {
ValueOperations<Object, Object> operation = redisTemplate.opsForValue();
operation.set(key, value, timeout, timeUnit);
return operation;
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public Object getCacheObject(Object key) {
ValueOperations<Object, Object> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key key
*/
public void deleteObject(Object key) {
redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection collection
*/
public void deleteObject(Collection<Object> collection) {
redisTemplate.delete(collection);
}
public Long getExpire(String key) {
return redisTemplate.getExpire(key);
}
public void expire(String key, int expire, TimeUnit timeUnit) {
redisTemplate.expire(key, expire, timeUnit);
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public ListOperations<Object, Object> setCacheList(Object key, List<Object> dataList) {
ListOperations<Object, Object> listOperations = redisTemplate.opsForList();
if (null != dataList) {
int size = dataList.size();
for (Object o : dataList) {
listOperations.leftPush(key, o);
}
}
return listOperations;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public List<Object> getCacheList(String key) {
List<Object> dataList = new ArrayList<>();
ListOperations<Object, Object> listOperation = redisTemplate.opsForList();
Long size = listOperation.size(key);
if (null != size) {
for (int i = 0; i < size; i++) {
dataList.add(listOperation.index(key, i));
}
}
return dataList;
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public BoundSetOperations<Object, Object> setCacheSet(String key, Set<Object> dataSet) {
BoundSetOperations<Object, Object> setOperation = redisTemplate.boundSetOps(key);
for (Object o : dataSet) {
setOperation.add(o);
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key key
* @return return
*/
public Set<Object> getCacheSet(Object key) {
Set<Object> dataSet = new HashSet<>();
BoundSetOperations<Object, Object> operation = redisTemplate.boundSetOps(key);
dataSet = operation.members();
return dataSet;
}
/**
* 缓存Map
*
* @param key key
* @param dataMap dataMap
* @return return
*/
public HashOperations<Object, Object, Object> setCacheMap(Object key, Map<Object, Object> dataMap) {
HashOperations<Object, Object, Object> hashOperations = redisTemplate.opsForHash();
if (null != dataMap) {
for (Map.Entry<Object, Object> entry : dataMap.entrySet()) {
hashOperations.put(key, entry.getKey(), entry.getValue());
}
}
return hashOperations;
}
/**
* 获得缓存的Map
*
* @param key key
* @return return
*/
public Map<Object, Object> getCacheMap(Object key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<Object> keys(String pattern) {
return redisTemplate.keys(pattern);
}
}
public class WebUtil {
/**
* 将字符串渲染到客户端
* @param resp resp
* @param str str
* @return return
*/
public static String renderString(HttpServletResponse resp, String str){
try{
resp.setStatus(200);
resp.setContentType("application/json");
resp.setCharacterEncoding("utf-8");
resp.getWriter().write(str);
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Ret<T> {
private Integer code;
private String msg;
private T data;
public static Ret ok(){
return new Ret(200,"ok",null);
}
public static Ret fail(){
return new Ret(200,"ok",null);
}
public Ret data(T data){
this.data = data;
return this;
}
}
引入mybatis-plus
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: "0101"
url: jdbc:mysql://localhost:3306/db_learn?charsetEncoding=utf8&serverTimeZone=UTC
数据库表与实体类
CREATE TABLE `sec_user` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`role` tinyint DEFAULT NULL,
`status` tinyint DEFAULT NULL,
PRIMARY KEY (`id`)
)
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sec_user")
public class User implements Serializable {
private static final long serialVersionUID = -4445259547431031783L;
@TableId
private Long id;
private String name;
private String password;
private Integer role;
private Integer status;
}
测试mybatisplus
@Test
public void testMybatisPlus(){
List<User> users = userMapper.selectList(null);
System.out.println(users);
}
校验用户
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getName, username);
User user = userMapper.selectOne(queryWrapper);
if (Objects.isNull(user)){
throw new RuntimeException("用户名或者密码错误");
}
// TODO 查询用户权限信息
// 把数据封装成UserDetail并返回
return new LoginUser(user);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private static final long serialVersionUID = -3470371303923656730L;
private User user;
/**
*
* @return 权限信息
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getName();
}
/**
*
* @return true -> 登录状态未过期
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
*
* @return true -> 帐户未锁定
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
*
* @return true -> 签发的凭据未过期
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
*
* @return true -> 该用户是正常的
*/
@Override
public boolean isEnabled() {
return true;
}
}
注意:如果要测试,需要往用户表中写入用户数据,并且如果你想让用户的密码是明文存储,需要在密码前加{noop}。例如
密码问题PasswordEncoder
实际项目中我们不会把密码明文存储在数据库中。
默认使用的PasswordEncoder要求数据库中的密码格式为: {id}password。 它会根据id去判断密码的加密方式。但是我们一般不会采这种方式。所以就需要替换
PasswordEncoder.
我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就 会使用该PasswordEncoder来进行密码校验。
我们可以定义-个SpringSecurity的配置类,SpringSecurity要求这 个配置类要继承WebSecurityConfigurerAdapter。
关于PasswordEncoder,每次加密的盐值不一样,相同的密码最后生成的密文不一样
加密
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode1 = passwordEncoder.encode("123456");
String encode2 = passwordEncoder.encode("123456");
System.out.println(encode1);
System.out.println(encode2);
输出
$2a$10$mivDryCWTsusAnEoqslzEO1Ucl4Cu/2yOfxPP0Q6BMVLpciOCcYlK
$2a$10$4.XsTRDY.U0AEPESe8ZZxu4VoCv3UnrrAejnQp.xC6EcDO6jmCQEu
比对
boolean b1 = passwordEncoder
.matches("123456", "$2a$10$mivDryCWTsusAnEoqslzEO1Ucl4Cu/2yOfxPP0Q6BMVLpciOCcYlK");
boolean b2 = passwordEncoder
.matches("1234", "$2a$10$mivDryCWTsusAnEoqslzEO1Ucl4Cu/2yOfxPP0Q6BMVLpciOCcYlK");
System.out.println(b1);// true
System.out.println(b2);// false
安装Redis for windows
安装 redis for windows
https://blog.csdn.net/qq_52385631/article/details/122771598
使用教程
https://blog.csdn.net/weixin_61594803/article/details/122695446
大概步骤
1.下载 redis for windows
2.安装,安装,一路next
(密码设置 安装目录下 redis.windows-service.conf 找到 requirepass ,并设置密码,格式 requirepass xxx)
3.启动redis服务,此电脑->管理->服务
4.安装目录下启动redis.cli,界面显示主机名端口
登录接口
自定义登登录接口,然后让SpringSecurity对这 个接口放行,让用户访问这个接口的时候不用登录也能访问。在接口中我们通过AuthenticationManager的
authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
认证成功的话要生成一个jwt, 放入响应中返回。并粗为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为
key
SecurityConfig重写两个方法
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 关闭csrf
.csrf().disable()
// 不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口,允许匿名访问
.antMatchers("/user/login").anonymous()
// 出上面以外,其他全要验证
.anyRequest().authenticated();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@RestController
public class LoginController {
@Resource
private LoginService loginService;
@PostMapping("/user/login")
public Ret login(@RequestBody User user){
return loginService.login(user);
}
}
@Service
public class LoginServiceImpl implements LoginService {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private RedisUtil redisUtil;
@Override
public Ret login(User user) {
// AuthenticationManager authenticate进行用户认证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getName(), user.getPassword());
Authentication authentication = authenticationManager.authenticate(authenticationToken);// 调用UserDetailServiceImpl.loadUserByUsername认证
// 未通过则给相应的提示
if (Objects.isNull(authentication)) {
throw new RuntimeException("登录失败");
}
// 认证通过,使用用户的id生成一个jwt
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long id = loginUser.getUser().getId();
String jwt = JwtUtil.createJwt(id.toString());
// 完整信息存入redis,用户的id作为key
redisUtil.setCacheObject("login:" + id, loginUser);
Map<String, String> map = new HashMap<>();
map.put("token", jwt);
return Ret.ok().data(map);
}
}
访问 http://localhost:8080/user/login
前提需要redis服务,否则失败
{
"code": 200,
"msg": "ok",
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxOTRhMzlmMTdlOGI0OTQyODcyZjAyODc2OGJlMDBmYiIsInN1YiI6IjEiLCJpc3MiOiJjdHAiLCJpYXQiOjE2NzA1OTUxOTIsImV4cCI6MTY3MDU5ODc5Mn0.5-m6NmJJRy4DpSbnxY1T-BFEE7vi4P0RB7-kVUD80Ns"
}
}
检验token
Claims claims = JwtUtil.parseJwt("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxOTRhMzlmMTdlOGI0OTQyODcyZjAyODc2OGJlMDBmYiIsInN1YiI6IjEiLCJpc3MiOiJjdHAiLCJpYXQiOjE2NzA1OTUxOTIsImV4cCI6MTY3MDU5ODc5Mn0.5-m6NmJJRy4DpSbnxY1T-BFEE7vi4P0RB7-kVUD80Ns");
String subject = claims.getSubject();
System.out.println(subject);// 1 (iuserId)
上面代码可用获取到用户id
token认证过滤器
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private RedisUtil redisUtil;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = httpServletRequest.getHeader("token");
if (StringUtils.isEmpty(token)){
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
// 解析token
String userId;
try {
Claims claims = JwtUtil.parseJwt(token);
userId = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("无效token");
}
// 从redis中获取用户信息
String redisKey = "login:" + userId;
LoginUser loginUser = (LoginUser)redisUtil.getCacheObject(redisKey);
if (Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
// 存入SecurityHolder
// TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, null);// authorities表示授权信息
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
添加到配置SecurityConfig.configure
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
退出登录接口
LoginServiceImpl
public Ret logout() {
// 获取SecurityContextHolder中的用户信息
UsernamePasswordAuthenticationToken authenticationToken =
(UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authenticationToken.getPrincipal();
Long userId = loginUser.getUser().getId();
// 删除redis中对应用户的信息
redisUtil.deleteObject("login:" + userId);
return Ret.ok().data("退出成功");
}
SecurityConfig
HttpSecurity.antMatchers("/user/login").anonymous()// 未登录可访问,登录不可访问(携带token,500错误)
HttpSecurity.antMatchers("/user/login").permitAll()// 不管是否登录都可以访问
HttpSecurity.anyRequest().authenticated(); // 登陆了就可以访问
授权引入
基本流程
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。 在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的
Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。所以我们在项目中只需要把当前登录用户的权限信息也存入
Authentication。然后设置我们的资源所需要的权限即可。
模拟授权
UserDetailServiceImpl.loadUserByUsername
...
// TODO 查询用户权限信息
List<String> list= new ArrayList<>(Arrays.asList("login","admin"));
// 把数据封装成UserDetail并返回
return new LoginUser(user,list);
JwtAuthenticationTokenFilter.doFilterInternal
// TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());// authorities表示授权信息
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
配置SecurityConfig
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {}
LoginController权限注解,下面表示有hello权限才能访问接口
@PostMapping("/hello")
@PreAuthorize("hasAnyAuthority('hello')")
public Ret hello(){
...
}
数据库权限与接口访问
数据库表
sec_user
role
user_role
permission
role_permission
根据用户id查询权限sql语句示例
select distinct `p`.`name`
from `user_role` ur
left join `role_permission` rp on `ur`.role_id = `rp`.role_id
left join `permission` p on `p`.id = `rp`.permisson_id
where `ur`.user_id = 1 and `p`.`name` is not null
-- 根据用户id 查询出角色id,然后根据角色id查询出拥有的权限id,再根据权限id查询出可用菜单集合(去重?用户可用有多个角色身份,可能存在权限重复)
PermissionMapper.java
List<String> selectPermsByUserId(@Param("userId") Long userId);
PermissionMapper.xml
<?xm1 version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.learnspringsecurity.mapper.PermissionMapper">
<select id="selectPermsByUserId" resultType="java.lang.String">
select distinct `p`.`name`
from `user_role` ur
left join `role_permission` rp on `ur`.role_id = `rp`.role_id
left join `permission` p on `p`.id = `rp`.permisson_id
where `ur`.user_id = #{userId} and `p`.`name` is not null
</select>
</mapper>
给相应接口定义所需要的权限
@RequestMapping("/hi")
@PreAuthorize("hasAnyAuthority('hi')")
public String hello(){
return "hi ~ ";
}
UserDetailServiceImpl.loadUserByUsername
List<String> list = permissionMapper.selectPermsByUserId(user.getId());
// 把数据封装成UserDetail并返回
return new LoginUser(user,list);
自定义失败处理
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败
还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即呵。
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
// 授权失败 异常处理
Ret<String> ret = new Ret<>(HttpStatus.FORBIDDEN.value(),"当前用户没有权限执行该操作",null);
String jsonString = JSON.toJSONString(ret);
WebUtil.renderString(httpServletResponse, jsonString);
}
}
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// 认证失败 异常处理
Ret<String> ret = new Ret<>(HttpStatus.UNAUTHORIZED.value(),"用户认证失败请重新登录",null);
String jsonString = JSON.toJSONString(ret);
WebUtil.renderString(httpServletResponse, jsonString);
}
}
SecurityConfig.configure
// 认证/授权 失败处理器
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
跨域
@Configuration
public class CrosConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 允许跨域请求
registry.addMapping("/**") // 允许的路径
.allowedOrigins("*") // 允许的域名
.allowCredentials(true) // 允许cookie?
.allowedMethods("GET","POST","DELETE","PUT");// 允许的请求方式
}
}
SecurityConfig.configure
// 允许跨域
http.cors();
权限校验方法
public abstract class SecurityExpressionRoot implements SecurityExpressionOperations {
protected final Authentication authentication;
private AuthenticationTrustResolver trustResolver;
private RoleHierarchy roleHierarchy;
private Set<String> roles;
private String defaultRolePrefix = "ROLE_";
public final boolean permitAll = true;
public final boolean denyAll = false;
private PermissionEvaluator permissionEvaluator;
public final String read = "read";
public final String write = "write";
public final String create = "create";
public final String delete = "delete";
public final String admin = "administration";
public SecurityExpressionRoot(Authentication authentication) {
if (authentication == null) {
throw new IllegalArgumentException("Authentication object cannot be null");
} else {
this.authentication = authentication;
}
}
public final boolean hasAuthority(String authority) {
return this.hasAnyAuthority(authority);
}
public final boolean hasAnyAuthority(String... authorities) {
return this.hasAnyAuthorityName((String)null, authorities);
}
public final boolean hasRole(String role) {
return this.hasAnyRole(role);
}
public final boolean hasAnyRole(String... roles) {
return this.hasAnyAuthorityName(this.defaultRolePrefix, roles);
}
private boolean hasAnyAuthorityName(String prefix, String... roles) {
Set<String> roleSet = this.getAuthoritySet();
String[] var4 = roles;
int var5 = roles.length;
for(int var6 = 0; var6 < var5; ++var6) {
String role = var4[var6];
String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
if (roleSet.contains(defaultedRole)) {
return true;
}
}
return false;
}
public final Authentication getAuthentication() {
return this.authentication;
}
public final boolean permitAll() {
return true;
}
public final boolean denyAll() {
return false;
}
public final boolean isAnonymous() {
return this.trustResolver.isAnonymous(this.authentication);
}
public final boolean isAuthenticated() {
return !this.isAnonymous();
}
public final boolean isRememberMe() {
return this.trustResolver.isRememberMe(this.authentication);
}
public final boolean isFullyAuthenticated() {
return !this.trustResolver.isAnonymous(this.authentication) && !this.trustResolver.isRememberMe(this.authentication);
}
public Object getPrincipal() {
return this.authentication.getPrincipal();
}
public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
this.trustResolver = trustResolver;
}
public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
this.roleHierarchy = roleHierarchy;
}
public void setDefaultRolePrefix(String defaultRolePrefix) {
this.defaultRolePrefix = defaultRolePrefix;
}
private Set<String> getAuthoritySet() {
if (this.roles == null) {
Collection<? extends GrantedAuthority> userAuthorities = this.authentication.getAuthorities();
if (this.roleHierarchy != null) {
userAuthorities = this.roleHierarchy.getReachableGrantedAuthorities(userAuthorities);
}
this.roles = AuthorityUtils.authorityListToSet(userAuthorities);
}
return this.roles;
}
public boolean hasPermission(Object target, Object permission) {
return this.permissionEvaluator.hasPermission(this.authentication, target, permission);
}
public boolean hasPermission(Object targetId, String targetType, Object permission) {
return this.permissionEvaluator.hasPermission(this.authentication, (Serializable)targetId, targetType, permission);
}
public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) {
this.permissionEvaluator = permissionEvaluator;
}
private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
if (role == null) {
return role;
} else if (defaultRolePrefix != null && defaultRolePrefix.length() != 0) {
return role.startsWith(defaultRolePrefix) ? role : defaultRolePrefix + role;
} else {
return role;
}
}
}
自定义权限校验方法
@Component("customExpressionRoot")
public class CustomExpressionRoot {
public boolean hasAuthority(String authority){
// 获取当前用户权限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
List<String> permissions = loginUser.getPermissions();
// 判断是否存在对应权限
return permissions.contains(authority);
}
}
在SPEL表达式中使用@ex相当于获取容器中bean的名字为customExpressionRoot的对象。然后再调用这个对象的hasAuthority方法
@RequestMapping("/custom")
@PreAuthorize("@customExpressionRoot.hasAuthority('custom')")
public Ret custom(){
return Ret.ok();
}
通过配置文件
接口路径与所需权限
HttpSecurity.antMatchers("/hi").hasAuthority("hi")
CSRF
CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。
https://blog.csdn.net/freeking101/article/details/86537087
SpringSecurity却防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf. token,前端发起请求的时候需要携带这个csrf_ token,后端会有过滤器进行校验,
如果没有携带或者是伪造的就不允许访问。我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,
而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。
认证成功处理
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().successHandler(authenticationSuccessHandler);
http.authorizeRequests().anyRequest().authenticated();
}
/**
* 作为bean放入容器中
* @return AuthenticationManager
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
@Component
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
System.out.println("认证成功了");
System.out.println(authentication.getPrincipal().toString());
}
}
当提交这个表单
输出
认证成功了
LoginUser(...
认证失败处理器
@Component
public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
System.out.println("认证失败了");
}
}
...
@Resource
private AuthenticationFailureHandler authenticationFailureHandler;
...
HttpSecurity.failureHandler(authenticationFailureHandler);
登出成功处理器
HttpSecurity.logout().logoutSuccessHandler(logoutSuccessHandler);
@Component
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
System.out.println("登出成功");
}
}