三星单片机开发网站,网页设计是什么概念,化工销售怎么做网站,汉字logo设计生成器SpringSecurity 文章目录 SpringSecurity一、 简介二、快速入门2.1 maven坐标2.2 访问请求 三、认证与授权3.1 认证3.1.1 登录检验流程3.1.2 SpringSecurity 完整流程3.1.3 认证流程详解3.1.4 校验3.1.5 要解决的问题3.1.6 准备工作3.1.7 实现3.1.7.1 数据库校验用户3.1.7.1.1 …SpringSecurity 文章目录 SpringSecurity一、 简介二、快速入门2.1 maven坐标2.2 访问请求 三、认证与授权3.1 认证3.1.1 登录检验流程3.1.2 SpringSecurity 完整流程3.1.3 认证流程详解3.1.4 校验3.1.5 要解决的问题3.1.6 准备工作3.1.7 实现3.1.7.1 数据库校验用户3.1.7.1.1 准备工作3.1.7.1.2 核心代码实现 3.1.7.2 密码加密存储3.1.7.3 自定义登录接口3.1.7.4 铺垫知识 jwt工具类使用3.1.7.5 Jwt 认证过滤器代码实现3.1.7.6 退出登录 3.2 授权3.2.1 权限系统的作用3.2.2 授权基本流程3.2.3 授权实现3.2.3.1 限制访问资源所需权限3.2.3.2 封装权限信息3.2.3.2.1 补充 UserDetailsServiceImpl implements UserDetailsService类授权3.2.3.2.2 补充 LoginUser implements UserDetails 类 授权3.2.3.2.3 补充 JwtAuthenticationTokenFilter extends OncePerRequestFilter 类 授权 3.2.3.3 从数据库查询权限信息3.2.3.3.1 RBAC权限模型3.2.3.3.2 建立权限表与角色表3.2.3.3.3 实体类3.2.3.3.4 补充 UserDetailsServiceImpl implements UserDetailsService类授权方法 四、 自定义失败处理4.1 自定义实现类4.1.1 自定义AuthenticationEntryPoint 提示认证失败4.1.2 自定义AccessDeniedHandler 提示授权失败 4.2 配置给SpringSecurity 五、跨域问题5.1 SpringBoot 配置5.2 开启SpringSecurity跨域访问 六、遗留问题6.1 其它权限校验方法6.2 自定义权限校验方法6.3 基于配置的权限控制6.4 CSRF6.5 认证成功处理器6.6 认证失败处理器6.7 注销成功处理器 一、 简介
SpringSecurity安全管理框架。相比于另外一个安全框架Shiro它提供了更丰富的功能社区资源也比Shiro丰富。
一般Web应用需要进行认证和授权而认证和授权是安全框架的核心功能。
认证验证当前访问系统的是不是本系统的用户并且要确认具体是哪个用户
授权经过认证后判断当前用户是否有权限进行某个操作
二、快速入门
2.1 maven坐标
!--引入SpringSecurity--
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-security/artifactId
/dependency引入依赖后我们在尝试去访问之前的接口就会自动跳转到一个SpringSecurity的默认登录界面默认用户名是User密码会输出在控制台
必须登陆之后才能对接口进行访问
2.2 访问请求
如下所示 当我们输入localhost:8080/hello/hello后并不能访问我们的请求
RestController
RequestMapping(/hello)
public class HelloController {GetMapping(/hello)private String hello(){return hello;}}而是出现了下面这个页面这个登录页面后面是可以换掉的前后端分离的话不需要登录页留一个登录接口就可以了。
默认的用户名 user 默认密码 会在控制台输出 并且控制台也出现了一串数字 使用Apifox也出现unauthorized没有权限 当我们在页面输入用户名以及密码后会获得我们请求的结果 三、认证与授权
3.1 认证
3.1.1 登录检验流程
核心依赖token(加密后的一个字符串)通过判断是否携带token可以判断是不是系统的用户也可以判断是哪一个用户。 3.1.2 SpringSecurity 完整流程
SpringSecurity的原理其实就是一个过滤器链内部包含了提供各种功能的过滤器 总共有15个过滤器 UsernamePasswordAuthenticationFilter: 负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要由它负责。 **ExceptionTranslationFilter: ** 处理认证和授权中出现的所有异常做统一的处理。 处理过滤器中抛出的任何AccessDeniedException和AuthenticationException。 FilterSecuritylnterceptor: 负责授权、负责权限校验的过滤器。并且判断当前访问的资源需要什么权限访问的具有什么权限是否能够访问。
3.1.3 认证流程详解
下图中的第一步我们登录提交的用户名和密码不会提交到这里我们会自己写一个controller然后在controller当中调用ProviderManager。
下图中的第五步我们要改成从数据库里面进行查询(下图中是在内存中查找)只需要把UserDetailsService接口的实现类InMemoryUserDetailsManager这个实现类换成其他的实现类就好了然后再调用这个实现类。 概念速查: Authentication接口: 它的实现类表示当前访问系统的用户封装了用户相关信息。 AuthenticationManager接口: 定义了认证Authentication的方法 UserDetailsService接口: 加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。 UserDetails接口: 提供核心用户信息通过UserDetailsservice根据用户名获取处理的用户信息要封装成 UserDetails对象返回。然后将这些信息封装到Authentication对象中。
分析后的最终结果 补充“登录接口”返回到前端之前的描述
如果认证通过使用用户id生成一个jwt然后用userid作为key用户信息作为value存入Redis。此处的token方便我们之后校验和授权。
3.1.4 校验
我们要对某些请求进行校验看看是否会有请求的权限。
我们需要自己定义过滤器解析前端带过来的token。 思考
JWT 认证过滤器中获取userid后怎么获取到完成的用户信息
也可以访问Service层再访问数据库但是每次请求都访问数据库会太浪费时间对数据库压力大。
这个地方我们可以加一个Redis从缓存中获取Redis中的信息我们可以每隔一段时间就更新。
那我们什么时候把token存入到Redis呢
在登录成功后。
3.1.5 要解决的问题
登录
① 自定义登录接口
调用ProviderManager的方法进行认证如果认证通过生成就 jwt并把信息存入Redis中
② 自定义UserDetailsService
在这个实现列中去查询数据库
检验
① 自定义Jwt认证过滤器
获取token解析token获取其中的userid从Redis中获取用户信息存入SecurityContextHolder中 SecurityContextHolder 对象作用 作用是保存和管理当前执行线程的安全上下文信息。安全上下文信息包括当前执行线程的身份验证、授权信息等安全相关的上下文数据。 SecurityContextHolder提供了一组静态方法来访问和管理当前执行线程的安全上下文信息如 getContext()获取当前执行线程的安全上下文对象。setContext(context)设置当前执行线程的安全上下文对象。createEmptyContext()创建一个新的空的安全上下文对象。clearContext()清除当前执行线程的安全上下文对象。 在Spring Security中开发人员可以通过SecurityContextHolder在应用程序任何地方访问和使用当前用户的身份和权限信息以便实现安全检查和控制。例如在方法调用时可以使用SecurityContextHolder获取当前用户的身份验证信息并确保该用户具有执行该方法所需的所有必要权限或者在记录日志时可以使用SecurityContextHolder获取当前用户身份验证信息将其添加到日志消息中以便跟踪特定用户的操作历史。 3.1.6 准备工作
①添加依赖 !--redis依赖--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependency!--fastjson依赖--dependencygroupIdcom.alibaba/groupIdartifactIdfastjson/artifactIdversion1.2.33/version/dependency!--jwt依赖--dependencygroupIdio.jsonwebtoken/groupIdartifactIdjjwt/artifactIdversion0.9.0/version/dependency② 添加Redis相关配置
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import com.alibaba.fastjson.parser.ParserConfig;
import org.springframework.util.Assert;
import java.nio.charset.Charset;/*** Redis使用FastJson序列化* * author sg*/
public class FastJsonRedisSerializerT implements RedisSerializerT
{public static final Charset DEFAULT_CHARSET Charset.forName(UTF-8);private ClassT clazz;static{ParserConfig.getGlobalInstance().setAutoTypeSupport(true);}public FastJsonRedisSerializer(ClassT clazz){super();this.clazz clazz;}Overridepublic byte[] serialize(T t) throws SerializationException{if (t null){return new byte[0];}return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);}Overridepublic T deserialize(byte[] bytes) throws SerializationException{if (bytes null || bytes.length 0){return null;}String str new String(bytes, DEFAULT_CHARSET);return JSON.parseObject(str, clazz);}protected JavaType getJavaType(Class? clazz){return TypeFactory.defaultInstance().constructType(clazz);}
}import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;Configuration
public class RedisConfig {BeanSuppressWarnings(value { unchecked, rawtypes })public RedisTemplateObject, Object redisTemplate(RedisConnectionFactory connectionFactory){RedisTemplateObject, Object template new RedisTemplate();template.setConnectionFactory(connectionFactory);FastJsonRedisSerializer serializer new FastJsonRedisSerializer(Object.class);// 使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(new StringRedisSerializer());template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;}
}③ 响应类
import com.fasterxml.jackson.annotation.JsonInclude;/*** Author 三更 B站 https://space.bilibili.com/663528522*/
JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResultT {/*** 状态码*/private Integer code;/*** 提示信息如果有错误时前端可以获取该字段进行提示*/private String msg;/*** 查询到的结果数据*/private T data;public ResponseResult(Integer code, String msg) {this.code code;this.msg msg;}public ResponseResult(Integer code, T data) {this.code code;this.data data;}public Integer getCode() {return code;}public void setCode(Integer code) {this.code code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg msg;}public T getData() {return data;}public void setData(T data) {this.data data;}public ResponseResult(Integer code, String msg, T data) {this.code code;this.msg msg;this.data data;}
}④工具类
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;/*** JWT工具类*/
public class JwtUtil {//有效期为public static final Long JWT_TTL 60 * 60 *1000L;// 60 * 60 *1000 一个小时//设置秘钥明文public static final String JWT_KEY sangeng;public static String getUUID(){String token UUID.randomUUID().toString().replaceAll(-, );return token;}/*** 生成jtw* param subject token中要存放的数据json格式* return*/public static String createJWT(String subject) {JwtBuilder builder getJwtBuilder(subject, null, getUUID());// 设置过期时间return builder.compact();}/*** 生成jtw* param subject token中要存放的数据json格式* param ttlMillis token超时时间* return*/public static String createJWT(String subject, Long ttlMillis) {JwtBuilder builder getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间return builder.compact();}private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {SignatureAlgorithm signatureAlgorithm SignatureAlgorithm.HS256;SecretKey secretKey generalKey();long nowMillis System.currentTimeMillis();Date now new Date(nowMillis);if(ttlMillisnull){ttlMillisJwtUtil.JWT_TTL;}long expMillis nowMillis ttlMillis;Date expDate new Date(expMillis);return Jwts.builder().setId(uuid) //唯一的ID.setSubject(subject) // 主题 可以是JSON数据.setIssuer(sg) // 签发者.setIssuedAt(now) // 签发时间.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥.setExpiration(expDate);}/*** 创建token* param id* param subject* param ttlMillis* return*/public static String createJWT(String id, String subject, Long ttlMillis) {JwtBuilder builder getJwtBuilder(subject, ttlMillis, id);// 设置过期时间return builder.compact();}public static void main(String[] args) throws Exception {String token eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg;Claims claims parseJWT(token);System.out.println(claims);}/*** 生成加密后的秘钥 secretKey* return*/public static SecretKey generalKey() {byte[] encodedKey Base64.getDecoder().decode(JwtUtil.JWT_KEY);SecretKey key new SecretKeySpec(encodedKey, 0, encodedKey.length, AES);return key;}/*** 解析** param jwt* return* throws Exception*/public static Claims parseJWT(String jwt) throws Exception {SecretKey secretKey generalKey();return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();}}import java.util.*;
import java.util.concurrent.TimeUnit;SuppressWarnings(value { unchecked, rawtypes })
Component
public class RedisCache
{Autowiredpublic RedisTemplate redisTemplate;/*** 缓存基本的对象Integer、String、实体类等** param key 缓存的键值* param value 缓存的值*/public T void setCacheObject(final String key, final T value){redisTemplate.opsForValue().set(key, value);}/*** 缓存基本的对象Integer、String、实体类等** param key 缓存的键值* param value 缓存的值* param timeout 时间* param timeUnit 时间颗粒度*/public T void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit){redisTemplate.opsForValue().set(key, value, timeout, timeUnit);}/*** 设置有效时间** param key Redis键* param timeout 超时时间* return true设置成功false设置失败*/public boolean expire(final String key, final long timeout){return expire(key, timeout, TimeUnit.SECONDS);}/*** 设置有效时间** param key Redis键* param timeout 超时时间* param unit 时间单位* return true设置成功false设置失败*/public boolean expire(final String key, final long timeout, final TimeUnit unit){return redisTemplate.expire(key, timeout, unit);}/*** 获得缓存的基本对象。** param key 缓存键值* return 缓存键值对应的数据*/public T T getCacheObject(final String key){ValueOperationsString, T operation redisTemplate.opsForValue();return operation.get(key);}/*** 删除单个对象** param key*/public boolean deleteObject(final String key){return redisTemplate.delete(key);}/*** 删除集合对象** param collection 多个对象* return*/public long deleteObject(final Collection collection){return redisTemplate.delete(collection);}/*** 缓存List数据** param key 缓存的键值* param dataList 待缓存的List数据* return 缓存的对象*/public T long setCacheList(final String key, final ListT dataList){Long count redisTemplate.opsForList().rightPushAll(key, dataList);return count null ? 0 : count;}/*** 获得缓存的list对象** param key 缓存的键值* return 缓存键值对应的数据*/public T ListT getCacheList(final String key){return redisTemplate.opsForList().range(key, 0, -1);}/*** 缓存Set** param key 缓存键值* param dataSet 缓存的数据* return 缓存数据的对象*/public T BoundSetOperationsString, T setCacheSet(final String key, final SetT dataSet){BoundSetOperationsString, T setOperation redisTemplate.boundSetOps(key);IteratorT it dataSet.iterator();while (it.hasNext()){setOperation.add(it.next());}return setOperation;}/*** 获得缓存的set** param key* return*/public T SetT getCacheSet(final String key){return redisTemplate.opsForSet().members(key);}/*** 缓存Map** param key* param dataMap*/public T void setCacheMap(final String key, final MapString, T dataMap){if (dataMap ! null) {redisTemplate.opsForHash().putAll(key, dataMap);}}/*** 获得缓存的Map** param key* return*/public T MapString, T getCacheMap(final String key){return redisTemplate.opsForHash().entries(key);}/*** 往Hash中存入数据** param key Redis键* param hKey Hash键* param value 值*/public T void setCacheMapValue(final String key, final String hKey, final T value){redisTemplate.opsForHash().put(key, hKey, value);}/*** 获取Hash中的数据** param key Redis键* param hKey Hash键* return Hash中的对象*/public T T getCacheMapValue(final String key, final String hKey){HashOperationsString, String, T opsForHash redisTemplate.opsForHash();return opsForHash.get(key, hKey);}/*** 删除Hash中的数据* * param key* param hkey*/public void delCacheMapValue(final String key, final String hkey){HashOperations hashOperations redisTemplate.opsForHash();hashOperations.delete(key, hkey);}/*** 获取多个Hash中的数据** param key Redis键* param hKeys Hash键集合* return Hash对象集合*/public T ListT getMultiCacheMapValue(final String key, final CollectionObject hKeys){return redisTemplate.opsForHash().multiGet(key, hKeys);}/*** 获得缓存的基本对象列表** param pattern 字符串前缀* return 对象列表*/public CollectionString keys(final String pattern){return redisTemplate.keys(pattern);}
}import javax.servlet.http.HttpServletResponse;
import java.io.IOException;public class WebUtils
{/*** 将字符串渲染到客户端* * param response 渲染对象* param string 待渲染的字符串* return null*/public static String renderString(HttpServletResponse response, String string) {try{response.setStatus(200);response.setContentType(application/json);response.setCharacterEncoding(utf-8);response.getWriter().print(string);}catch (IOException e){e.printStackTrace();}return null;}
}⑤实体类
import java.io.Serializable;
import java.util.Date;/*** 用户表(User)实体类** author 三更*/
Data
AllArgsConstructor
NoArgsConstructor
public class User implements Serializable {private static final long serialVersionUID -40356785423868312L;/*** 主键*/private Long id;/*** 用户名*/private String userName;/*** 昵称*/private String nickName;/*** 密码*/private String password;/*** 账号状态0正常 1停用*/private String status;/*** 邮箱*/private String email;/*** 手机号*/private String phonenumber;/*** 用户性别0男1女2未知*/private String sex;/*** 头像*/private String avatar;/*** 用户类型0管理员1普通用户*/private String userType;/*** 创建人的用户id*/private Long createBy;/*** 创建时间*/private Date createTime;/*** 更新人*/private Long updateBy;/*** 更新时间*/private Date updateTime;/*** 删除标志0代表未删除1代表已删除*/private Integer delFlag;
}3.1.7 实现
3.1.7.1 数据库校验用户
从之前的分析我们可以知道我们可以自定义一个UserDetailsService,让SpringSecurity使用我们的UserDetailsService。我们自己的UserDetailsService可以从数据库中查询用户名和密码。
3.1.7.1.1 准备工作
我们先创建一个用户表 建表语句如下
CREATE TABLE sys_user (id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 主键,user_name VARCHAR(64) NOT NULL DEFAULT NULL COMMENT 用户名,nick_name VARCHAR(64) NOT NULL DEFAULT NULL COMMENT 昵称,password VARCHAR(64) NOT NULL DEFAULT NULL COMMENT 密码,status CHAR(1) DEFAULT 0 COMMENT 账号状态0正常 1停用,email VARCHAR(64) DEFAULT NULL COMMENT 邮箱,phonenumber VARCHAR(32) DEFAULT NULL COMMENT 手机号,sex CHAR(1) DEFAULT NULL COMMENT 用户性别0男1女2未知,avatar VARCHAR(128) DEFAULT NULL COMMENT 头像,user_type CHAR(1) NOT NULL DEFAULT 1 COMMENT 用户类型0管理员1普通用户,create_by BIGINT(20) DEFAULT NULL COMMENT 创建人的用户id,create_time DATETIME DEFAULT NULL COMMENT 创建时间,update_by BIGINT(20) DEFAULT NULL COMMENT 更新人,update_time DATETIME DEFAULT NULL COMMENT 更新时间,del_flag INT(11) DEFAULT 0 COMMENT 删除标志0代表未删除1代表已删除,PRIMARY KEY (id)
) ENGINEINNODB AUTO_INCREMENT2 DEFAULT CHARSETutf8mb4 COMMENT用户表 引入MybatisPuls和mysql驱动的依赖 dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-boot-starter/artifactIdversion3.4.3/version/dependencydependencygroupIdcom.alibaba/groupIdartifactIddruid-spring-boot-starter/artifactIdversion1.2.8/version/dependencydependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdversion8.0.32/version/dependency
配置数据库信息
spring:datasource:druid:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/springsecurity?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf-8zeroDateTimeBehaviorconvertToNulluseSSLfalseallowPublicKeyRetrievaltrueusername: rootpassword: rootmybatis-plus:configuration:map-underscore-to-camel-case: true#sqllog-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:db-config:id-type: ASSIGN_ID
3.1.7.1.2 核心代码实现
定义UserDetailsService的实现类我们上图中的第五步
/*** 与数据库进行操作*/
Service
public class UserDetailsServiceImpl implements UserDetailsService {Autowiredprivate UserMapper userMapper;// 可以观看之前粉色的那张图片这个方法是由DaoAuthenticationProvider调用
// 我们要在这方法中做的就是 想数据库中查询获取用户信息、查询权限信息Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// TODO 查询用户信息LambdaQueryWrapperUser queryWrapper new LambdaQueryWrapper();queryWrapper.eq(User::getUserName,username);User user userMapper.selectOne(queryWrapper);
// 如果没有查询到用户就抛出异常if(Objects.isNull(user)){throw new RuntimeException(用户或者密码错误);}// TODO 查询对应的权限信息讲到授权后在补全这个地方// TODO 封装成UserDetails将其返回// LoginUser是我们自己封装的一个UserDetails接口的实现类return new LoginUser(user);}
}Data
NoArgsConstructor
AllArgsConstructor
public class LoginUser implements UserDetails {private User user;/**** return 获取权限信息*/Overridepublic Collection? extends GrantedAuthority getAuthorities() {
// 返回权限信息的return null;}/*** 框架会调用LoginUser的getPassword方法获取当前用户的密码* return 获取当前用户的密码*/Overridepublic String getPassword() {return user.getPassword();}/**** return*/Overridepublic String getUsername() {return user.getUserName();}/*** 判断是否没过期的** return false 代表超时*/Overridepublic boolean isAccountNonExpired() {return true;}Overridepublic boolean isAccountNonLocked() {return true;}Overridepublic boolean isCredentialsNonExpired() {return true;}/*** 是否可用** return*/Overridepublic boolean isEnabled() {return true;}
}注意我们需要预先在数据库中添加数据。如果想让用户密码是铭文存储则需要再密码前加{noop}
3.1.7.2 密码加密存储
实际项目中我们不会把密码明文存储在数据库中。
默认使用的PasswordEncoder要求数据库中的密码格式为: {id}password 它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要*替换PasswordEncoder。
我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中Springsecurity就会使用该PasswordEncoder来进行密码校验。
我们可以定义一个SpringSecurity的配置类SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter,
当我们配置好下面的实体类后
Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {/**** return 创建 BCryptPasswordEncoder 注入容器*/Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}
}其中PasswordEncoder如下所示
Autowired
private PasswordEncoder passwordEncoder ; //使用的时候这么注入就可以了不用向下面那样创建encode 传入一个密码的原文就会帮我们加密。指的注意的是即使我们的明文是一个样的但生成的密文也有可能不一样。
BCryptPasswordEncoder bCryptPasswordEncoder new BCryptPasswordEncoder();
String encode bCryptPasswordEncoder.encode(1234);
System.out.println(encode); //$2a$10$2z2HZ5ewLSyV9DqoyyHXB.4U8DVlPsfVQqgi61683XYEIQEJkL78y**matches**进行密码校验的。传入一个想校验的密码比如用户登录时输入的密码再传入一个加密的密码(数据库存储的密码密文)。 3.1.7.3 自定义登录接口
按照我们之前“3.1.5 要解决的问题”中自定义登录接口调用ProviderManager的方法进行认证如果认证通过生成jwt把用户信息存入Redis中
自定义登录接口让SpringSecurity对这个接口进行放行让用户访问这个接口的时候不用登录也能访问。
Override //这里不是Bean 在public class SecurityConfig extends WebSecurityConfigurerAdapte类中
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();
}在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证所以需要在SecurityConfig中配置把AuthenticationManager注入容器。如下面的第二个方法
Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {/*** 密码加密解密* return 创建 BCryptPasswordEncoder 注入容器*/Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}/**** return 在SecurityConfig中配置把AuthenticationManager注入容器。* throws Exception*/Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}认证成功的话生成一个jwt放入响应中放回。并且为了让用户下回请求时能通过jwt识别出具体哪个用户我们需要把用户信息存入Redis可以把用户id作为key
Service
public class LoginServiceImpl implements LoginService {Autowiredprivate AuthenticationManager authenticationManager;Autowiredprivate RedisCache redisCache;Overridepublic ResponseResult login(User user) {
// TODO 通过AuthenticationManager的authenticate方法来进行用户认证
// 需要Authentication类型(接口)的参数,我们可以使用Authentication的实现类UsernamePasswordAuthenticationTokenUsernamePasswordAuthenticationToken authenticationToken new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());Authentication authenticate authenticationManager.authenticate(authenticationToken);// TODO 如果认证没通过给出对应的提示if (Objects.isNull(authenticate)) {throw new RuntimeException(登录失败);}// TODO 如果认证通过了使用userId生成一个jwt封装成ResponseResult对象进行返回
// 这个地方为什么能强转成LoginUser类型
// UserDetailsServiceImpl类实现了UserDetailsService并且重写了loadUserByUsername方法其方法返回值是UserDetails
// 但是创建了一个类LoginUser实现了UserDetails接口LoginUser loginUser (LoginUser)authenticate.getPrincipal();Long id loginUser.getUser().getId();String jwt JwtUtil.createJWT(id.toString());// 希望date数据中是 key:value的形式所以用个mapMapString,String map new HashMap();map.put(token,jwt);// TODO 把完整的用户信息存入Redis, userId作为keyredisCache.setCacheObject(login:id,loginUser);return new ResponseResult(200,登录成功,map);}
}3.1.7.4 铺垫知识 jwt工具类使用
/*** JWT工具类*/
public class JwtUtil {//有效期为public static final Long JWT_TTL 60 * 60 *1000L;// 60 * 60 *1000 一个小时//设置秘钥明文public static final String JWT_KEY sangeng;public static String getUUID(){String token UUID.randomUUID().toString().replaceAll(-, );return token;}/*** 生成jtw* param subject token中要存放的数据json格式* return*/public static String createJWT(String subject) {JwtBuilder builder getJwtBuilder(subject, null, getUUID());// 设置过期时间return builder.compact();}/*** 生成jtw* param subject token中要存放的数据json格式* param ttlMillis token超时时间* return*/public static String createJWT(String subject, Long ttlMillis) {JwtBuilder builder getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间return builder.compact();}private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {SignatureAlgorithm signatureAlgorithm SignatureAlgorithm.HS256;SecretKey secretKey generalKey();long nowMillis System.currentTimeMillis();Date now new Date(nowMillis);if(ttlMillisnull){ttlMillisJwtUtil.JWT_TTL;}long expMillis nowMillis ttlMillis;Date expDate new Date(expMillis);return Jwts.builder().setId(uuid) //唯一的ID.setSubject(subject) // 主题 可以是JSON数据.setIssuer(sg) // 签发者.setIssuedAt(now) // 签发时间.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥.setExpiration(expDate);}/*** 创建token* param id* param subject* param ttlMillis* return*/public static String createJWT(String id, String subject, Long ttlMillis) {JwtBuilder builder getJwtBuilder(subject, ttlMillis, id);// 设置过期时间return builder.compact();}public static void main(String[] args) throws Exception {String token eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg;Claims claims parseJWT(token);System.out.println(claims);}/*** 生成加密后的秘钥 secretKey* return*/public static SecretKey generalKey() {byte[] encodedKey Base64.getDecoder().decode(JwtUtil.JWT_KEY);SecretKey key new SecretKeySpec(encodedKey, 0, encodedKey.length, AES);return key;}/*** 解析** param jwt* return* throws Exception*/public static Claims parseJWT(String jwt) throws Exception {SecretKey secretKey generalKey();return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();}
}3.1.7.5 Jwt 认证过滤器代码实现
根据之前的分析在这里我们要实现
① 获取token
② 解析token获取其中的userid
③ 从Redis中获取用户信息
④ 存入SecurityContextHolder
/*** 之前我们选择的是实现Filter接口但是这个过滤器接口存在一点问题有可能发一次请求经过好几次过滤器* OncePerRequestFilter 是过滤器的实现类一次请求只经过一个过滤器*/
Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {Autowiredprivate RedisCache redisCache;Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// TODO 获取token (前端发送请求携带过来)String token request.getHeader(token); //有可能是空的不一定所有的请求都携带tokenif (!Strings.hasText(token)) {
// 说明token没有直接放行
// 为什么放行 因为后面的操作是对token的解析而这个请求没有携带token
// 除此之外后面还有其他的过滤器也可以在进行判断比如说在FilterSecurityInterceptor中filterChain.doFilter(request, response);
// 为什么加return 放行后会执行到后面的几个过滤器都执行完然后响应的时候还会执行一遍过滤器链return;}// TODO 解析tokenClaims claims null;String userId null;try {claims JwtUtil.parseJWT(token);
// 这样获取的就是生成token的原来数据因为当时我们使用userid生成的tokenuserId claims.getSubject();} catch (Exception e) {e.printStackTrace();throw new RuntimeException(token非法);}// TODO 从Redis中获取用户信息String redisKey login:userId;LoginUser loginUser redisCache.getCacheObject(redisKey);if (loginUser null ){throw new RuntimeException(token非法);}// TODO 将用户信息存入到SecurityContextHolder中
// 三个参数在构造方法中会有一个super.setAuthenticated(true)表示已认证的情况
// 第一个参数用户信息第二个参数null,第三个参数Collection集合有关权限的信息但是现在还没有权限信息先写nullUsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken new UsernamePasswordAuthenticationToken(loginUser,null,null);SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);// TODO: 放行filterChain.doFilter(request, response);}
}虽然我们把这个过滤器链写好了但是此过滤器并不会在SpringSecurity当中并且要指定过滤器在过滤器链中的位置我们需要自己进行配置 Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers(/user/login).anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();
// 添加过滤器http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);}3.1.7.6 退出登录
用户之前生成的token不能使用了。 Overridepublic ResponseResult logout() {
// TODO 获取SecurityContextHolder中的用户idUsernamePasswordAuthenticationToken authentication (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser (LoginUser) authentication.getPrincipal();Long id loginUser.getUser().getId();// TODO 删除Redis中的值redisCache.deleteObject(login:id);return new ResponseResult(200,注销成功);}3.2 授权
微信来举例子微信登录成功后用户即可使用微信的功能比如发红包、!发朋友圈、添加好友等没有绑定银行卡的用户是无法发送红包的绑定银行卡的用户才可以发红包发红包功能、发朋友圈功能都是微信的资源即功能资源用户拥有发红包功能的权限才可以正常使用发送红包功能拥有发朋友圈功能的权限才可以使用发朋友圈功能这个根据用户的权限来控制用户使用资源的过程就是授权。
为什么要授权 ? 认证是为了保证用户身份的合法性授权则是为了更细粒度的对隐私数据进行划分授权是在认证通过后发生的控制不同的用户能够访问不同的资源。
**授权:**授权是用户认证通过根据用户的权限来控制用户访问资源的过程拥有资源的访问权限则正常访问没有权限则拒绝访问。
3.2.1 权限系统的作用
例如一个学校图书馆的管理系统如果是普通学生登录就能看到借书还书相关的功能不可能让他看到并且去使用添加书籍信息删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了应该就能看到并使用添加书籍信息删除书籍信息等功能。 总结起来就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。
我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样如果有人知道了对应功能的接口地址就可以不通过前端直接去发送请求来实现相关功能操作。
所以我们还需要在后台进行用户权限的判断判断当前用户是否有相应的权限必须具有所需权限才能进行相应的操作。
3.2.2 授权基本流程
在SpringSecurity中会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
我们之前的图
3.2.3 授权实现
3.2.3.1 限制访问资源所需权限
我们选择基于注解对权限控制的方式开启相关配置
Configuration
EnableGlobalMethodSecurity(prePostEnabled true) //开启注解的功能
public class SecurityConfig extends WebSecurityConfigurerAdapter {..................
}此时可以使用对应的注解
RestController
RequestMapping(/hello)
public class HelloController {GetMapping(/hello)
// 会执行hasAuthority(test)方法返回值类型是布尔类型如果是true就可以访问这个请求PreAuthorize(hasAuthority(test)) //访问资源之前进行一个资源的认证是否能够访问这个资源private String hello(){return hello;}
}3.2.3.2 封装权限信息
3.2.3.2.1 补充 UserDetailsServiceImpl implements UserDetailsService类授权
/*** 与数据库进行操作*/
Service
public class UserDetailsServiceImpl implements UserDetailsService {Autowiredprivate UserMapper userMapper;// 可以观看之前粉色的那张图片这个方法是由DaoAuthenticationProvider调用
// 我们要在这方法中做的就是 想数据库中查询获取用户信息、查询权限信息Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// TODO 查询用户信息LambdaQueryWrapperUser queryWrapper new LambdaQueryWrapper();queryWrapper.eq(User::getUserName,username);User user userMapper.selectOne(queryWrapper);
// 如果没有查询到用户就抛出异常if(Objects.isNull(user)){throw new RuntimeException(用户不存在);}// TODO 查询对应的权限信息讲到授权后在补全这个地方
// 这个地方我们先把权限信息写死ListString list new ArrayList(Arrays.asList(test,admin));// TODO 封装成UserDetails将其返回
// LoginUser是我们自己封装的一个UserDetails接口的实现类return new LoginUser(user,list); //传入用户信息及权限集合我们现在对LoginUser进行了修改}
}
3.2.3.2.2 补充 LoginUser implements UserDetails 类 授权
Data
NoArgsConstructor
AllArgsConstructor
public class LoginUser implements UserDetails {private User user;// 存储权限信息private ListString permissions;// 为什么什么这个成员变量
// 如果每次请求都把权限字符串封装成下面的代码比较耗时间我们直接把他设置成成员变量JSONField(serialize false) //这个属性不会序列化到我们的Redis中private ListSimpleGrantedAuthority authorities;public LoginUser(User user) {this.user user;}public LoginUser(User user, ListString permissions) {this.user user;this.permissions permissions;}/**** return 获取权限信息*/Overridepublic Collection? extends GrantedAuthority getAuthorities() {
// 为什么什么这个成员变量
// 如果每次请求都把权限字符串封装成下面的代码比较耗时间我们直接把他设置成成员变量// 把permissions集合的String类型权限封装成Collection? extends GrantedAuthority的实现类SimpleGrantedAuthorityif(authorities !null){return authorities;}authorities permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
// 返回权限信息的return authorities;}/*** 框架会调用LoginUser的getPassword方法获取当前用户的密码* return 获取当前用户的密码*/Overridepublic String getPassword() {return user.getPassword();}/**** return*/Overridepublic String getUsername() {return user.getUserName();}/*** 判断是否没过期的** return false 代表超时*/Overridepublic boolean isAccountNonExpired() {return true;}Overridepublic boolean isAccountNonLocked() {return true;}Overridepublic boolean isCredentialsNonExpired() {return true;}/*** 是否可用** return*/Overridepublic boolean isEnabled() {return true;}
}3.2.3.2.3 补充 JwtAuthenticationTokenFilter extends OncePerRequestFilter 类 授权
*** 之前我们选择的是实现Filter接口但是这个过滤器接口存在一点问题有可能发一次请求经过好几次过滤器* OncePerRequestFilter 是过滤器的实现类一次请求只经过一个过滤器*/
Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {Autowiredprivate RedisCache redisCache;Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// TODO 获取token (前端发送请求携带过来)String token request.getHeader(token); //有可能是空的不一定所有的请求都携带tokenif (!Strings.hasText(token)) {
// 说明token没有直接放行
// 为什么放行 因为后面的操作是对token的解析而这个请求没有携带token
// 除此之外后面还有其他的过滤器也可以在进行判断比如说在FilterSecurityInterceptor中filterChain.doFilter(request, response);
// 为什么加return 放行后会执行到后面的几个过滤器都执行完然后响应的时候还会执行一遍过滤器链return;}// TODO 解析tokenClaims claims null;String userId null;try {claims JwtUtil.parseJWT(token);
// 这样获取的就是生成token的原来数据因为当时我们使用userid生成的tokenuserId claims.getSubject();} catch (Exception e) {e.printStackTrace();throw new RuntimeException(token非法);}// TODO 从Redis中获取用户信息String redisKey login:userId;LoginUser loginUser redisCache.getCacheObject(redisKey);if (loginUser null ){throw new RuntimeException(token非法);}// TODO 将用户信息存入到SecurityContextHolder中、获取权限信息封装到Authentication中
// 三个参数在构造方法中会有一个super.setAuthenticated(true)表示已认证的情况
// 第一个参数用户信息第二个参数null,第三个参数Collection集合有关权限的信息但是现在还没有权限信息先写nullUsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);// TODO: 放行filterChain.doFilter(request, response);}
}3.2.3.3 从数据库查询权限信息
刚刚我们用户的权限是从代码中写死的我们现在要把用户对应的权限放入到数据库然后查询获取对应权限
3.2.3.3.1 RBAC权限模型
RBAC权限模型基于角色的权限控制这是目前最常被开发者使用也是相对易用、通用权限模型。
一个角色就是一个角色组比如管理员角色有什么权限普通用户有什么权限…这样的话我们就给用户分配角色就可以了。 3.2.3.3.2 建立权限表与角色表
值得注意的是用户可以有多个角色可以使图书管理员也可以是借阅人角色表也对应了多个用户即用户表和角色表是多对多的关系。
用户表与角色表关联角色表与权限表关联。
DROP TABLE IF EXISTS sys_menu;CREATE TABLE sys_menu (id bigint(20) NOT NULL AUTO_INCREMENT,menu_name varchar(64) NOT NULL DEFAULT NULL COMMENT 菜单名,path varchar(200) DEFAULT NULL COMMENT 路由地址,component varchar(255) DEFAULT NULL COMMENT 组件路径,visible char(1) DEFAULT 0 COMMENT 菜单状态0显示 1隐藏,status char(1) DEFAULT 0 COMMENT 菜单状态0正常 1停用,perms varchar(100) DEFAULT NULL COMMENT 权限标识,icon varchar(100) DEFAULT # COMMENT 菜单图标,create_by bigint(20) DEFAULT NULL,create_time datetime DEFAULT NULL,update_by bigint(20) DEFAULT NULL,update_time datetime DEFAULT NULL,del_flag int(11) DEFAULT 0 COMMENT 是否删除0未删除 1已删除,remark varchar(500) DEFAULT NULL COMMENT 备注,PRIMARY KEY (id)
) ENGINEInnoDB AUTO_INCREMENT2 DEFAULT CHARSETutf8mb4 COMMENT菜单表;/*Table structure for table sys_role */DROP TABLE IF EXISTS sys_role;CREATE TABLE sys_role (id bigint(20) NOT NULL AUTO_INCREMENT,name varchar(128) DEFAULT NULL,role_key varchar(100) DEFAULT NULL COMMENT 角色权限字符串,status char(1) DEFAULT 0 COMMENT 角色状态0正常 1停用,del_flag int(1) DEFAULT 0 COMMENT del_flag,create_by bigint(200) DEFAULT NULL,create_time datetime DEFAULT NULL,update_by bigint(200) DEFAULT NULL,update_time datetime DEFAULT NULL,remark varchar(500) DEFAULT NULL COMMENT 备注,PRIMARY KEY (id)
) ENGINEInnoDB AUTO_INCREMENT3 DEFAULT CHARSETutf8mb4 COMMENT角色表;/*Table structure for table sys_role_menu */DROP TABLE IF EXISTS sys_role_menu;CREATE TABLE sys_role_menu (role_id bigint(200) NOT NULL AUTO_INCREMENT COMMENT 角色ID,menu_id bigint(200) NOT NULL DEFAULT 0 COMMENT 菜单id,PRIMARY KEY (role_id,menu_id)
) ENGINEInnoDB AUTO_INCREMENT2 DEFAULT CHARSETutf8mb4;/*Table structure for table sys_user */DROP TABLE IF EXISTS sys_user;CREATE TABLE sys_user (id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键,user_name varchar(64) NOT NULL DEFAULT NULL COMMENT 用户名,nick_name varchar(64) NOT NULL DEFAULT NULL COMMENT 昵称,password varchar(64) NOT NULL DEFAULT NULL COMMENT 密码,status char(1) DEFAULT 0 COMMENT 账号状态0正常 1停用,email varchar(64) DEFAULT NULL COMMENT 邮箱,phonenumber varchar(32) DEFAULT NULL COMMENT 手机号,sex char(1) DEFAULT NULL COMMENT 用户性别0男1女2未知,avatar varchar(128) DEFAULT NULL COMMENT 头像,user_type char(1) NOT NULL DEFAULT 1 COMMENT 用户类型0管理员1普通用户,create_by bigint(20) DEFAULT NULL COMMENT 创建人的用户id,create_time datetime DEFAULT NULL COMMENT 创建时间,update_by bigint(20) DEFAULT NULL COMMENT 更新人,update_time datetime DEFAULT NULL COMMENT 更新时间,del_flag int(11) DEFAULT 0 COMMENT 删除标志0代表未删除1代表已删除,PRIMARY KEY (id)
) ENGINEInnoDB AUTO_INCREMENT3 DEFAULT CHARSETutf8mb4 COMMENT用户表;/*Table structure for table sys_user_role */DROP TABLE IF EXISTS sys_user_role;CREATE TABLE sys_user_role (user_id bigint(200) NOT NULL AUTO_INCREMENT COMMENT 用户id,role_id bigint(200) NOT NULL DEFAULT 0 COMMENT 角色id,PRIMARY KEY (user_id,role_id)
) ENGINEInnoDB DEFAULT CHARSETutf8mb4;SELECT DISTINCT m.perms FROM sys_user_role ur LEFT JOIN sys_role r ON ur.role_id r.id LEFT JOIN sys_role_menu rm ON ur.role_id rm.role_id LEFT JOIN sys_menu m ON m.id rm.menu_id WHERE user_id 2 AND r.status 0 AND m.status 0
3.2.3.3.3 实体类
/*** 菜单表(Menu)实体类*/
TableName(valuesys_menu)
Data
AllArgsConstructor
NoArgsConstructor
JsonInclude(JsonInclude.Include.NON_NULL)
public class Menu implements Serializable {private static final long serialVersionUID -54979041104113736L;TableIdprivate Long id;/*** 菜单名*/private String menuName;/*** 路由地址*/private String path;/*** 组件路径*/private String component;/*** 菜单状态0显示 1隐藏*/private String visible;/*** 菜单状态0正常 1停用*/private String status;/*** 权限标识*/private String perms;/*** 菜单图标*/private String icon;private Long createBy;private Date createTime;private Long updateBy;private Date updateTime;/*** 是否删除0未删除 1已删除*/private Integer delFlag;/*** 备注*/private String remark;
}Mapper
public interface MenuMapper extends BaseMapperMenu {Select(SELECT \n \tDISTINCT m.perms\n FROM\n \tsys_user_role ur\n \tLEFT JOIN sys_role r ON ur.role_id r.id\n \tLEFT JOIN sys_role_menu rm ON ur.role_id rm.role_id\n \tLEFT JOIN sys_menu m ON m.id rm.menu_id\n WHERE\n \tuser_id #{userId}\n \tAND r.status 0\n \tAND m.status 0)ListString selectPermsByUserId(Long userId);
}3.2.3.3.4 补充 UserDetailsServiceImpl implements UserDetailsService类授权方法
Service
public class UserDetailsServiceImpl implements UserDetailsService {Autowiredprivate UserMapper userMapper;Autowiredprivate MenuMapper menuMapper;// 可以观看之前粉色的那张图片这个方法是由DaoAuthenticationProvider调用
// 我们要在这方法中做的就是 想数据库中查询获取用户信息、查询权限信息Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// TODO 查询用户信息LambdaQueryWrapperUser queryWrapper new LambdaQueryWrapper();queryWrapper.eq(User::getUserName,username);User user userMapper.selectOne(queryWrapper);
// 如果没有查询到用户就抛出异常if(Objects.isNull(user)){throw new RuntimeException(用户不存在);}// TODO 查询对应的权限信息讲到授权后在补全这个地方ListString list menuMapper.selectPermsByUserId(user.getId());
// ListString list new ArrayList(Arrays.asList(test,admin)); //写死// TODO 封装成UserDetails将其返回
// LoginUser是我们自己封装的一个UserDetails接口的实现类return new LoginUser(user,list); //传入用户信息及权限集合}
}RestController
RequestMapping(/hello)
public class HelloController {GetMapping(/hello)
// 会执行hasAuthority(test)方法返回值类型是布尔类型如果是true就可以访问这个请求PreAuthorize(hasAuthority(system:dept:list)) //访问资源之前进行一个资源的认证是否能够访问这个资源private String hello(){return hello;}
}四、 自定义失败处理
希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的ison这样可以让前端能对响
应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。
在SpringSecurity中如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntrvPoint对象的方法去进行异常外理
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理.
所以如果我们需要自定义异常处理我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
4.1 自定义实现类
4.1.1 自定义AuthenticationEntryPoint 提示认证失败
Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {ResponseResult r new ResponseResult(HttpStatus.UNAUTHORIZED.value(),用户认证失败。请重新登录); //认证失败String json JSON.toJSONString(r);
// 不论成功还是失败都是JSON格式WebUtils.renderString(response,json);}
}4.1.2 自定义AccessDeniedHandler 提示授权失败
Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {ResponseResult r new ResponseResult(HttpStatus.FORBIDDEN.value(),权限不足); //认证失败String json JSON.toJSONString(r);
// 不论成功还是失败都是JSON格式WebUtils.renderString(response,json); // 封装的方法}
}4.2 配置给SpringSecurity
所以如果我们需要自定义异常处理我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
Configuration
EnableGlobalMethodSecurity(prePostEnabled true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {/*** 密码加密解密* return 创建 BCryptPasswordEncoder 注入容器*/Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;Autowiredprivate AuthenticationEntryPoint AuthenticationEntryPoint;Autowiredprivate AccessDeniedHandler AccessDeniedHandler;Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers(/user/login).anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();
// TODO 添加过滤器http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);// TODO 配置异常处理器
// 添加AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurityhttp.exceptionHandling()
// 认证失败处理器.authenticationEntryPoint( AuthenticationEntryPoint)
// 授权失败处理器.accessDeniedHandler(AccessDeniedHandler);}/**** return 在SecurityConfig中配置把AuthenticationManager注入容器。* throws Exception*/Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}五、跨域问题
浏览器出于安全的考虑使用XMLHttpRequest对象发起http请求时必须遵守同源策略否则就是跨域HTTP请求默认情况下是被禁止的。同源策略要求源相同才能正常进行通信即协议、域名、端口号都完全一致。
前后端分离项目前端项目和后端项目一般都不是同源的所以肯定会存在跨域请求问题。 之前解决是使用代理服务器 Vue实战——使用代理服务器解决跨域问题——No‘Access-Control-Allow-Origin‘ header is present on the requested resource_vue代理跨域_我爱布朗熊的博客-CSDN博客 5.1 SpringBoot 配置
Configuration
public class CorsConfig implements WebMvcConfigurer {Overridepublic void addCorsMappings(CorsRegistry registry) {// 设置允许跨域的路径registry.addMapping(/**)// 设置允许跨域请求的域名.allowedOriginPatterns(*)// 是否允许cookie.allowCredentials(true)// 设置允许的请求方式.allowedMethods(GET, POST, DELETE, PUT)// 设置允许的header属性.allowedHeaders(*)// 跨域允许时间.maxAge(3600);}
}或者参照下面一段代码
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;Configuration
public class MvcConfig implements WebMvcConfigurer {Beanpublic CorsFilter corsFilter(){CorsConfiguration configuration new CorsConfiguration();configuration.setAllowCredentials(true); //是否允许携带cookieconfiguration.addAllowedOrigin(*); //设置访问路径configuration.addAllowedHeader(*); //设置访问源请求头configuration.addAllowedMethod(*); //设置访问源请求方法configuration.setMaxAge(1800L); //有效期1800秒//添加映射路径拦截一切请求UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration(/**,configuration);//返回新的CorsFilterreturn new CorsFilter(source);}
}
5.2 开启SpringSecurity跨域访问 Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers(/user/login).anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();
// TODO 添加过滤器http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);// TODO 配置异常处理器
// 添加AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurityhttp.exceptionHandling()
// 认证失败处理器.authenticationEntryPoint( AuthenticationEntryPoint)
// 授权失败处理器.accessDeniedHandler(AccessDeniedHandler);
// TODO 允许跨域http.cors();}六、遗留问题
6.1 其它权限校验方法
我们前面都是使用PreAuthorize注解然后在在其中使用的是hasAuthority方法进行校验。SpringSecurity还为我们提供了其它方法例如hasAnyAuthorityhasRolehasAnyRole等。
这里我们先不急着去介绍这些方法我们先去理解hasAuthority的原理然后再去学习其他方法你就更容易理解而不是死记硬背区别。并且我们也可以选择定义校验方法实现我们自己的校验逻辑。
hasAuthority方法实际是执行到了SecurityExpressionRoot的hasAuthority大家只要断点调试既可知道它内部的校验原理。
它内部其实是调用authentication的getAuthorities方法获取用户的权限列表。然后判断我们存入的方法参数数据在权限列表中。
hasAnyAuthority方法可以传入多个权限只有用户有其中任意一个权限都可以访问对应资源。 PreAuthorize(hasAnyAuthority(admin,test,system:dept:list))public String hello(){return hello;} hasRole要求有对应的角色才可以访问但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。 PreAuthorize(hasRole(system:dept:list))public String hello(){return hello;} hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。 PreAuthorize(hasAnyRole(admin,system:dept:list))public String hello(){return hello;}6.2 自定义权限校验方法
我们也可以定义自己的权限校验方法在PreAuthorize注解中使用我们的方法。
Component(ex) //bean的名字为ex
public class SGExpressionRoot {public boolean hasAuthority(String authority){//获取当前用户的权限Authentication authentication SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser (LoginUser) authentication.getPrincipal();ListString permissions loginUser.getPermissions();//判断用户权限集合中是否存在authorityreturn permissions.contains(authority);}
} 在SPEL表达式中使用 ex相当于获取容器中bean的名字未ex的对象。然后再调用这个对象的hasAuthority方法 RequestMapping(/hello)PreAuthorize(ex.hasAuthority(system:dept:list))public String hello(){return hello;}6.3 基于配置的权限控制
我们也可以在配置类中使用使用配置的方式对资源进行权限控制。 Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers(/user/login).anonymous() //TODO 匿名访问不需要验证.antMatchers(/testCors).hasAuthority(system:dept:list222) //TODO 在这里进行配置的// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();//添加过滤器http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//配置异常处理器http.exceptionHandling()//配置认证失败处理器.authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);//允许跨域http.cors();}6.4 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攻击也就不用担心了。
前后端分离项目天然不怕CSRF攻击的所以我们在最开始配置的时候是csrf().disable()
6.5 认证成功处理器
实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候如果登录成功了是会调用AuthenticationSuccessHandler的方法进行认证成功后的处理的。AuthenticationSuccessHandler就是登录成功处理器。
我们也可以自己去自定义成功处理器进行成功后的相应处理。
Component
public class SGSuccessHandler implements AuthenticationSuccessHandler {Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {System.out.println(认证成功了);}
}
Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {Autowiredprivate AuthenticationSuccessHandler successHandler;Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin().successHandler(successHandler);http.authorizeRequests().anyRequest().authenticated();}
}
6.6 认证失败处理器
实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候如果认证失败了是会调用AuthenticationFailureHandler的方法进行认证失败后的处理的。AuthenticationFailureHandler就是登录失败处理器。
我们也可以自己去自定义失败处理器进行失败后的相应处理。
Component
public class SGFailureHandler implements AuthenticationFailureHandler {Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {System.out.println(认证失败了);}
}Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {Autowiredprivate AuthenticationSuccessHandler successHandler;Autowiredprivate AuthenticationFailureHandler failureHandler;Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin()
// 配置认证成功处理器.successHandler(successHandler)
// 配置认证失败处理器.failureHandler(failureHandler);http.authorizeRequests().anyRequest().authenticated();}
}
6.7 注销成功处理器
Component
public class SGLogoutSuccessHandler implements LogoutSuccessHandler {Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {System.out.println(注销成功);}
}
Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {Autowiredprivate AuthenticationSuccessHandler successHandler;Autowiredprivate AuthenticationFailureHandler failureHandler;Autowiredprivate LogoutSuccessHandler logoutSuccessHandler;Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin()
// 配置认证成功处理器.successHandler(successHandler)
// 配置认证失败处理器.failureHandler(failureHandler);http.logout()//配置注销成功处理器.logoutSuccessHandler(logoutSuccessHandler);http.authorizeRequests().anyRequest().authenticated();}
}