SpringSecurity 详解
- 1、SpringSecurity讲解
- 1.1、SpringSecurity完整流程
- 1.2、认证流程
- 2、登录,退出,注册_分析说明
- 2.1、登录
- 2.2、校验
- 2.3、退出
- 2.4、注册
- 2.5、SecurityContextHolder说明
- 3、代码实现
- 3.1、引入依赖
- 3.2、登录 退出 注册
- 3.2.1、SpringSecurity配置目录结构
- 3.2.2、登录退出_controller + service
- BlogLoginServiceImpl(service)
- SecurityConfig——SpringSecurity配置类
- UserDetailsServiceImpl(UserDetailsService接口实现类)
- JwtAuthenticationTokenFilter 登录校验过滤器 (JWT认证过滤器)
- AuthenticationEntryPoint ——认证成功处理器
- AccessDeniedHandler ——认证失败处理器
- 3.2.3、注册
- UserServiceImpl
- 3.2.4、工具类
- BeanCopyUtils
- JwtUtil
- RedisCache
- SecurityUtils
- WebUtils
- PathUtils
1、SpringSecurity讲解
1.1、SpringSecurity完整流程
- Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
- AuthenticationManager接口: Authentication()_认证方法
- UserDetailsService接口: loadUserByUsername()_根据用户名查询用户信息
- UserDetails接口:提供核心用户信息。通过UserDetailsService的 loadUserByUsername()拿到用户信息封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
- UserDetailsService接口和UserDetails接口合二为一
1.2、认证流程
2、登录,退出,注册_分析说明
SpringSecurity安全认证 登陆 退出 注册
1.登录
1.1、第一次登陆时,根据userId生成jwt(能反解析出userId),返回给 浏览器并存储
1.2、用户下次再请求时,带上token, 登录校验过滤器会从token中解析出userId,
并通过这个key(userId) 在redis查询 key对应的 value(loginUser).
1.3、 如果找到,说明已经登录(将认证信息即用户信息 存入SecurityContextHolder)。 如果查不到,说明没有登录,需要重新登录
2、退出,从SecurityContextHolder中拿到认证信息loginUser,进而得到userId。 通过这个key(userId) 从redis中删除
3、 注册,使用BCryptPasswordEncoder密码加密
2.1、登录
①自定义登录接口
- 调用ProviderManager的方法进行认证 如果认证通过生成jwt
- 把用户信息存入redis中
②自定义UserDetailsService
实现类UserDetailsServiceImpl implements UserDetailsService
-
在实现类UserDetailsServiceImpl 中重写loadUserByUsername() 去数据库查询 用户名密码
-
查到(将用户信息,权限信息 封装 返回类型UserDetails)
注意配置passwordEncoder为BCryptPasswordEncoder
2.2、校验
①定义Jwt认证过滤器 (**登录认证过滤器**)
-
获取token
-
解析token获取其中的userid
-
通过userid查redis,查到(从redis中获取用户信息),查不到 认证失败(重新登录)
-
存入SecurityContextHolder(SpringSecurity的组件 存储 Authentication认证信息)
2.3、退出
- 从SecurityContextHolder中拿到认证信息loginUser,进而得到userId 在redis 中删除
这个key(userId)对应的键值对
2.4、注册
- 密码加密(BCryptPasswordEncoder方式)
- 数据库插入数据
2.5、SecurityContextHolder说明
- SecurityContextHolder默认使用ThreadLocal 策略来存储 认证信息. 与线程绑定的策略
- Spring Security在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。
- 在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
3、代码实现
3.1、引入依赖
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
3.2、登录 退出 注册
3.2.1、SpringSecurity配置目录结构
3.2.2、登录退出_controller + service
BlogLoginServiceImpl(service)
package com.sangeng.service.impl;
import com.sangeng.domain.ResponseResult;
import com.sangeng.domain.entity.LoginUser;
import com.sangeng.domain.entity.User;
import com.sangeng.domain.vo.BlogUserLoginVo;
import com.sangeng.domain.vo.UserInfoVo;
import com.sangeng.service.BlogLoginService;
import com.sangeng.utils.BeanCopyUtils;
import com.sangeng.utils.JwtUtil;
import com.sangeng.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.Objects;
@Service
public class BlogLoginServiceImpl implements BlogLoginService {
@Autowired
private AuthenticationManager authenticationManager;
//SecurityConfig类中配置AuthenticationManager注入到容器中,所有这里可以使用AuthenticationManager
//接口AuthenticationManager 实现类ProviderManager 调用Authentication()进行认证
@Autowired
private RedisCache redisCache; //封装好的工具类,使用redis, 本质:public RedisTemplate redisTemplate;
@Override
public ResponseResult login(User user) {
// 接口authenticationManager.authenticate()进行认证 如果认证通过生成jwt (ProviderManager是接口authenticationManager的实现类)
// UsernamePasswordAuthenticationToken (父)-> AbstractAuthenticationToken -> Authentication(子)
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());//传用户名 密码
Authentication authenticate = authenticationManager.authenticate(authenticationToken);//调用认证方法 参数是Authentication(实现类UsernamePasswordAuthenticationToken)
// authenticationManager 实际上 会默认调用UserDetailsService接口去认证,所以要重写UserDetailsService接口
//security会使用UserDetailsService实现类中的loadUserByUsername方法进行校验,所以要重写该方法
// return new LoginUser(user) 返回给 Authentication authenticate,包含了 UserDetails对象(权限信息 + 用户信息)
// authenticationManager.authenticate() 认证的时候,自动进行密码比对
//判断是否通过
if (Objects.isNull(authenticate)){
throw new RuntimeException("用户名或密码错误");
}
//获取userid,生成token Authentication对象:(username,password) ->认证(数据库查找用户名密码+用户信息+权限信息) UserDetailsService.loadUserByUsername()实现
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();//获取认证主体 强转成LoginUser
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId); //jwt:把userid加密后的密文 即token 可以解析token拿到userid
//把用户信息(用户+权限)存入redis 格式(bloglogin:id,loginUser) (包含用户信息+权限信息)
redisCache.setCacheObject("bloglogin:"+userId,loginUser);
/** redisCache.setCacheObject()方法 是工具类封装好的方法,等同于如下代码
@Autowired
public RedisTemplate redisTemplate;
public <T> void setCacheObject(final String key, final T value){
redisTemplate.opsForValue().set(key, value);
}
*/
//把token和userinfo封装 返回
//把User转换成UserInfoVO
UserInfoVo userInfoVo = BeanCopyUtils.copyBean(loginUser.getUser(), UserInfoVo.class);
BlogUserLoginVo vo = new BlogUserLoginVo(jwt,userInfoVo); //{ token ,userInfo}
// 响应:登录成功后,返回给浏览器一个token,下次再请求的时候,需要带上这个token即可,即可识别出哪个具体的用户
// 为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把userId作为key,即 (userId,loginUser)
/** SpringSecurity安全认证 登陆 退出
* 第一次登陆时,根据userId生成jwt(能反解析出userId),返回给 浏览器并存储
* 用户下次再请求时,带上token, 登录校验过滤器会从token中解析出userId,
* 并通过这个key(userId) 在redis查询 key对应的 value(loginUser).
* 如果找到,说明已经登录(将认证信息即用户信息 存入SecurityContextHolder)。 如果查不到,说明没有登录,需要重新登录
* 退出时,从SecurityContextHolder中拿到认证信息loginUser,进而得到userId。 通过这个key(userId) 从redis中删除
*/
return ResponseResult.okResult(vo);
}
@Override
public ResponseResult logout() {
//获取token 解析token
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();//从SecurityContextHolder中 拿到Authentication对象
//SecurityContextHolder默认使用ThreadLocal 策略来存储 认证信息. 与线程绑定的策略
// Spring Security在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。
//当用户再次发送请求时,携带token,则自动把token存入到 SecurityContextHolder
LoginUser loginUser = (LoginUser) authentication.getPrincipal(); //获取认证主体
//获取userid
Long userId = loginUser.getUser().getId();
// 删除redis中的用户信息
redisCache.deleteObject("bloglogin:"+userId);
// redisTemplate.delete(key);
return ResponseResult.okResult();
}
}
SecurityConfig——SpringSecurity配置类
package com.sangeng.config;
import com.sangeng.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
// 为啥注入的不是实现类呢? 不传实现类是为了符合开闭原则
@Autowired
AuthenticationEntryPoint authenticationEntryPoint; //认证成功处理器
@Autowired
AccessDeniedHandler accessDeniedHandler; //认证失败处理器
@Bean
public PasswordEncoder passwordEncoder(){ //对比密码时 加密方式 要改成BCryptPasswordEncoder()
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous() //接口必须携带token
.antMatchers("/logout").authenticated()//退出接口 必须认证,即必须携带token才能 发出退出请求
.antMatchers("/user/userInfo").authenticated() //这个接口需要认证之后才能访问
// .antMatchers("/upload").authenticated() //前端vue上传图片时,没有要求token,所以后端不需要认证,不需要传token即可
// .antMatchers("/link/getAllLink").authenticated() //这个接口需要认证之后才能访问
// 除上面外的所有请求全部不需要认证即可访问
.anyRequest().permitAll();
//配置异常处理器
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint) //认证失败处理器
.accessDeniedHandler(accessDeniedHandler); //授权失败处理器
http.logout().disable(); //关闭默认 logout功能
//将过滤器 配置到 UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//允许跨域
http.cors();
}
@Override
@Bean //暴露ProvideManager方法注入到spring bean容器中
public AuthenticationManager authenticationManagerBean() throws Exception {
// 这一段配置用于登录时认证,只有使用了这个配置才能自动注入AuthenticationManager,并使用它来进行用户认证
// 使用的时候,直接注入此bean对象即可使用 @Autowired private AuthenticationManager authenticationManager;
return super.authenticationManagerBean();
}
}
UserDetailsServiceImpl(UserDetailsService接口实现类)
package com.sangeng.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.sangeng.constants.SystemConstants;
import com.sangeng.domain.entity.LoginUser;
import com.sangeng.domain.entity.User;
import com.sangeng.mapper.MenuMapper;
import com.sangeng.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.List;
import java.util.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private MenuMapper menuMapper;
/**
* 通过用户名 查找用户
* @param username
* @return UserDetails 对象
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//security会使用UserDetailsService实现类中的loadUserByUsername方法进行校验
//根据用户名查询用户信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(queryWrapper);
//判断是否查到用户 如果没查到抛出异常
if (Objects.isNull(user)){ throw new RuntimeException("用户不存在"); }
//返回用户信息 返回的是UserDatails对象,后面才做密码校验 密码SpringSecurity自动校验
//TODO 查询权限信息封装 如果是后台用户才需要查询权限封装 (前台用户不需要查询权限)
if (user.getType().equals(SystemConstants.ADMIN)){ //如果是管理员,返回 用户信息+权限信息
List<String> perms = menuMapper.selectPermsByUserId(user.getId()); //权限列表
return new LoginUser(user,perms);
}
// 定义一个loginUser 实现 UserDetails,即可返回
return new LoginUser(user,null); //UserDetails对象(权限信息 + 用户信息)
}
}
JwtAuthenticationTokenFilter 登录校验过滤器 (JWT认证过滤器)
package com.sangeng.filter;
import com.alibaba.fastjson.JSON;
import com.sangeng.domain.ResponseResult;
import com.sangeng.domain.entity.LoginUser;
import com.sangeng.enums.AppHttpCodeEnum;
import com.sangeng.utils.JwtUtil;
import com.sangeng.utils.RedisCache;
import com.sangeng.utils.WebUtils;
import io.jsonwebtoken.Claims;
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;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 登录校验过滤器 定义Jwt认证过滤器 浏览器登录后,再次发送请求时,会携带token,让服务器验证
//获取请求头中的token
String token = request.getHeader("token");
if(!StringUtils.hasText(token)){ //没有token就说明是第一次登录,直接放行
//说明该接口 不需要登录,直接放行
filterChain.doFilter(request,response);
return; //放行,程序到此结束
}
//解析token 获取userid
Claims claims = null;
try {
claims = JwtUtil.parseJWT(token); //解析token 得到userId
} catch (Exception e) {
e.printStackTrace();
//token超时,token非法
//响应告诉前端重新登录 (响应一个json格式)
ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);// 401 请重新登录
WebUtils.renderString(response, JSON.toJSONString(result)); //转成json,并把json串写到响应体当中
return;
}
String userId = claims.getSubject(); // 拿到userid
//从redis获取用户信息(如果获取失败,则验证失败)
LoginUser loginUser = redisCache.getCacheObject("bloglogin:" + userId);
//如果获取不到
if (Objects.isNull(loginUser)){
//说明登录过期,重新登录
ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);// 401 请重新登录
WebUtils.renderString(response, JSON.toJSONString(result)); //转成json,并把json串写到响应体当中
return;
}
//登录成功,将用户信息loginUser 存入SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);//未认证,用两个参数,认证过,用三个参数
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 都执行完了要 放行,让下面的filter处理
filterChain.doFilter(request,response);
}
}
AuthenticationEntryPoint ——认证成功处理器
package com.sangeng.handler.security;
import com.alibaba.fastjson.JSON;
import com.sangeng.domain.ResponseResult;
import com.sangeng.enums.AppHttpCodeEnum;
import com.sangeng.utils.WebUtils;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
// AuthenticationEntryPoint 认证失败处理器
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
authException.printStackTrace(); //打印异常信息
// BadCredentialsException
// InsufficientAuthenticationException
//认证失败 会 抛出BadCredentialsException异常
ResponseResult result = null;
if (authException instanceof BadCredentialsException) {
result = ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_ERROR.getCode(), authException.getMessage());
} else if (authException instanceof InsufficientAuthenticationException) {
result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
}else {
result = ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR,"认证或授权失败");//给出错误的提示信息
}
//转化为json响应给前端 (认证失败后,响应给前端 自定义的 json格式字符串)
WebUtils.renderString(response, JSON.toJSONString(result));
}
}
AccessDeniedHandler ——认证失败处理器
package com.sangeng.handler.security;
import com.alibaba.fastjson.JSON;
import com.sangeng.domain.ResponseResult;
import com.sangeng.enums.AppHttpCodeEnum;
import com.sangeng.utils.WebUtils;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
// AccessDeniedHandler 授权失败处理器
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
accessDeniedException.printStackTrace();
ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NO_OPERATOR_AUTH);
//响应给前端
WebUtils.renderString(response, JSON.toJSONString(result));
}
}
3.2.3、注册
UserServiceImpl
package com.sangeng.service.impl;
import ...
@Service("userService")
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder;
//SecurityConfig中配置类中将PasswordEncoder(对密码加密处理)注入到容器中,这里才能使用
@Override
public ResponseResult register(User user) {
//对数据进行非空判断 ("" 或者 null)
if (!StringUtils.hasText(user.getUserName())){
throw new SystemException(AppHttpCodeEnum.USERNAME_NOT_NULL);
}
if (!StringUtils.hasText(user.getPassword())){
throw new SystemException(AppHttpCodeEnum.PASSWORD_NOT_NULL);
}
if (!StringUtils.hasText(user.getEmail())){
throw new SystemException(AppHttpCodeEnum.EMAIL_NOT_NULL);
}
if (!StringUtils.hasText(user.getNickName())){
throw new SystemException(AppHttpCodeEnum.NICKNAME_NOT_NULL);
}
//对数据进行 是否存在判断 (用户名,邮箱是否已存在)
if (userNameExist(user.getUserName())){
throw new SystemException(AppHttpCodeEnum.USERNAME_EXIST);
}
if (emailExist(user.getEmail())){
throw new SystemException(AppHttpCodeEnum.EMAIL_EXIST);
}
// 配置类SecurityConfig中 设置加密方式 (不使用默认加密方式)
// @Bean
// public PasswordEncoder passwordEncoder(){ //对比密码时 加密方式 要改成BCryptPasswordEncoder()
// return new BCryptPasswordEncoder();
// }
//对密码进行加密
String encodePassword = passwordEncoder.encode(user.getPassword());//对明文密码 进行加密,得到密文
user.setPassword(encodePassword); //将password加密后的密文,存到user中
//存入数据库中
save(user); // IService中的方法 mybatisplus提供
return ResponseResult.okResult();
}
3.2.4、工具类
BeanCopyUtils
package com.sangeng.utils;
import org.springframework.beans.BeanUtils;
import java.util.List;
import java.util.stream.Collectors;
public class BeanCopyUtils {
// 构造方法设置为私有的方法
private BeanCopyUtils() {
}
// 单个实体类拷贝(将一个源对象 拷贝 至 字节码class)
// 通过反射创建目标对象,然后再拷贝
public static <V> V copyBean(Object source,Class<V> clazz) {
//创建目标对象 传过来什么类型,就返回什么类型,使用泛型( <V> V泛型方法,返回值类型 )
V result = null; //提升作用域
try {
result = clazz.newInstance();
//实现属性copy
BeanUtils.copyProperties(source, result);
} catch (Exception e) {
throw new RuntimeException(e);
}
//返回结果
return result;
}
// 集合拷贝
// (后面要用)声明泛型O,V 返回类型 public <T> void say(){} 表明是泛型方法
public static <O,V> List<V> copyBeanList(List<O> list, Class<V> clazz){
// 先将list集合 转成流->流当中元素的转换(转换方式copyBean方法) 返回一个泛型V -> 收集操作,泛型转成list
return list.stream()
.map(o -> copyBean(o, clazz))
.collect(Collectors.toList());
}
// 测试使用方法
// public static void main(String[] args) {
// Article article = new Article();
// article.setId(1L);
// article.setTitle("hello");
//
// HotArticleVo hotArticleVo = copyBean(article, HotArticleVo.class);
// System.out.println(hotArticleVo);
// }
}
JwtUtil
package com.sangeng.utils;
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 = 24*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(ttlMillis==null){
ttlMillis=JwtUtil.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();
}
}
RedisCache
package com.sangeng.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
@Autowired
public 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)
{
ValueOperations<String, 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 List<T> dataList)
{
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key)
{
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
{
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key)
{
return redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, 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)
{
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* redis 是一个键值对的NoSQL数据库 结构={ key,value=hash{hkey,value} }
* @param key
* @param hkey
* @param v
*/
public void incrementCacheMapValue(String key , String hkey, int v){
// v 表示递增值 浏览量每次增加几
redisTemplate.opsForHash().increment(key,hkey,v); //遍历Map, 找到 key对应的 hash(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> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
{
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern)
{
return redisTemplate.keys(pattern);
}
}
SecurityUtils
package com.sangeng.utils;
import com.sangeng.domain.entity.LoginUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
/**
* @Author 三更 B站: https://space.bilibili.com/663528522
*/
public class SecurityUtils
{
/**
* 获取用户
**/
public static LoginUser getLoginUser()
{
return (LoginUser) getAuthentication().getPrincipal();
}
/**
* 获取Authentication
*/
public static Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}
public static Boolean isAdmin(){
Long id = getLoginUser().getUser().getId();
return id != null && id.equals(1L);
}
public static Long getUserId() {
return getLoginUser().getUser().getId();
}
}
WebUtils
package com.sangeng.utils;
import org.springframework.web.context.request.RequestContextHolder;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
public class WebUtils
{
/**
* 将字符串渲染到客户端
* @param response 渲染对象
* @param string 待渲染的字符串
* @return null
*/
public static void 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();
}
}
public static void setDownLoadHeader(String filename, HttpServletResponse response) throws UnsupportedEncodingException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fname= URLEncoder.encode(filename,"UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition","attachment; filename="+fname);
}
}
PathUtils
package com.sangeng.utils;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
/**
* @Author 三更 B站: https://space.bilibili.com/663528522
*/
public class PathUtils {
public static String generateFilePath(String fileName){ //fileName为原始文件名 111.png
//根据日期生成路径 2022/1/15/
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd/");//日期格式化
String datePath = sdf.format(new Date());
//uuid作为文件名 生成uuid
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
//后缀和文件后缀一致
int index = fileName.lastIndexOf("."); //找到111.png中.的索引
// test.jpg -> .jpg
String fileType = fileName.substring(index); //截取index后面的字符串 [index, ... )
// 拼接 2022/1/15/+uuid+.jpg 即可得到文件路径
return new StringBuilder().append(datePath).append(uuid).append(fileType).toString();
}
}