前后端所用技术栈:vue2+springboot2
权限认证:jwt+spring security
jwt:通过它获取令牌并传递给前端,实现一次登录,多次访问的功能。(这里可以稍微说的简洁点,但是jwt优点以及令牌结构需要自行百度)
spring security:通过它可以很简单的控制用户的访问权限以及授权功能
这里,认证流程和授权流程需要了解下
认证流程:1.spring security认证流程_苏七qaq的博客-CSDN博客_springsecurity授权流程
授权流程:3.spring security授权流程_苏七qaq的博客-CSDN博客
jwt代码块:
package com.example.blog.utils;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JWTUtils {
/*
* jwt 有三部分组成:A.B.C
A:Header,{"type":"JWT","alg":"HS256"} 固定
B:playload,存放信息,比如,用户id,过期时间等等,可以被解密,不能存放敏感信息
C: 签证,A和B加上秘钥 加密而成,只要秘钥不丢失,可以认为是安全的。
jwt 验证,主要就是验证C部分 是否合法。
* */
private static final String jwtToken = "123456Mszlu!@#$$";
public static String createToken(Long userId) {
/*B部分*/
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
/*使用JWT构建builder对象*/
JwtBuilder jwtBuilder = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法,秘钥为jwtToken
.setClaims(claims) // body数据,要唯一,自行设置
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000));// 一天的有效时间
String token = jwtBuilder.compact();
return token;
}
public static Map<String, Object> checkToken(String token) {
try {
Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);
return (Map<String, Object>) parse.getBody();//返回B部分
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
spring security代码块:
package com.example.admin.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//使用BCrypt加密策略
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
public static void main(String[] args)
{
//加密策略 MD5 不安全 彩虹表 MD5 加盐
String mszlu = new BCryptPasswordEncoder().encode("mszlu");
}
@Override
public void configure(WebSecurity web) throws Exception
{
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception
{
//开启登录认证
http.authorizeRequests()
.antMatchers("/css/**").permitAll()
.antMatchers("/img/**").permitAll()
.antMatchers("/js/**").permitAll()
.antMatchers("/plugins/**").permitAll()
.antMatchers("/admin/**").access("@authService.auth(request,authentication)") //自定义service实现实时的权限认证
.antMatchers("/pages/**").authenticated();
//没有权限,会自动跳到登陆页,需要开启登录的页面
http.formLogin()
.loginPage("/login.html") //自定义的登录页面
.loginProcessingUrl("/login") //登录处理接口
.usernameParameter("username") //定义登录时的用户名的key 默认为username
.passwordParameter("password") //定义登录时的密码key,默认是password
.defaultSuccessUrl("/pages/main.html") //登录成功跳转的页面
.failureUrl("/login.html") //登录失败跳转的页面
.permitAll(); //和登录表单相关的接口/login通过
//退出登录配置
http.logout()
.logoutUrl("/logout") //退出登录接口
.logoutSuccessUrl("/login.html") //退出登录成功跳转的页面
.permitAll(); //退出登录的接口放行
http.httpBasic()
.and()
.csrf().disable() //csrf关闭 如果自定义登录 需要关闭
.headers().frameOptions().sameOrigin();//支持iframe嵌套
}
}
package com.example.admin.service;
import com.example.admin.pojo.Admin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
@Service
public class SecurityUserService implements UserDetailsService
{
@Autowired
private AdminService adminService;
//登录的时候,会把username传递到这里
//通过username查询admin,如果存在,将密码告诉springSecurity
//如果不存在 返回Null,代表认证失败
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
Admin admin = adminService.findAdminByUsernameAndPassword(username,null);
if (admin == null)
{
//登录失败
return null;
}
UserDetails userDetails = new User(username, admin.getPassword(), new ArrayList<>());
return userDetails;
}
}
package com.example.admin.service;
import com.example.admin.pojo.Admin;
import com.example.admin.pojo.Permission;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.thymeleaf.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
//自定义service 来去实现实时的权限认证
@Service
public class AuthService
{
@Autowired
private AdminService adminService;
//权限认证
public boolean auth(HttpServletRequest request, Authentication authentication)
{
//请求路径
String requestURI = request.getRequestURI();
//匿名用户
Object principal = authentication.getPrincipal();
if (principal == null || "anonymousUser".equals(principal))
{
//未登录
return false;
}
UserDetails userDetails = (UserDetails) principal;
String username = userDetails.getUsername();
String password = userDetails.getPassword();
Admin admin = adminService.findAdminByUsernameAndPassword(username,password);
if (admin == null)
{
return false;
}
//最高权限者
if (admin.getId() == 1)
{
return true;
}
Long id = admin.getId();
List<Permission> permissionList = adminService.findPermissionByAdminId(id);
//requestURI可能有?后面的参数传参,所以要分割一下
requestURI = StringUtils.split(requestURI, "?")[0];
for (Permission permission : permissionList) {
//判断该用户有没有这个请求路径的权限
if (requestURI.equals(permission.getPath())) {
return true;
}
}
return false;
}
}
数据库:mysql5.7+redis
redis:对令牌和用户信息的管理,进一步增加了安全性能,登录用户和接口做了缓存,灵活控制用户和接口缓存的过期时间
用户信息的获取:threadLocal
threadLocal用于保存用户的信息,请求的线程之内,可以随时获取登录的用户,做了线程隔离。用完threadLocal之后,做了value的删除,防止内存的泄露
threadLocal代码块:
package com.example.blog.utils;
import com.example.blog.entity.SysUser;
public class UserThreadLocal {
/*不希望通过new获取UserThreadLocal对象*/
private UserThreadLocal() {
}
//线程变量隔离
private static final ThreadLocal<SysUser> LOCAL = new ThreadLocal<>();
public static void put(SysUser sysUser) {
LOCAL.set(sysUser);
}
public static SysUser get() {
return LOCAL.get();
}
public static void remove() {
LOCAL.remove();
}
}
package com.example.blog.cofig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync //开启多线程
public class ThreadPoolConfig {
@Bean("taskExcutor")
public Executor asyncServiceExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数
executor.setCorePoolSize(5);
// 设置最大线程数
executor.setMaxPoolSize(20);
//配置队列大小
executor.setQueueCapacity(Integer.MAX_VALUE);
// 设置线程活跃时间(秒)
executor.setKeepAliveSeconds(60);
// 设置默认线程名称
executor.setThreadNamePrefix("博客");
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
//执行初始化
executor.initialize();
return executor;
}
}
利用线程池更新文章阅读数:
package com.example.blog.service;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.example.blog.dao.mapper.ArticleMapper;
import com.example.blog.entity.Article;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class ThreadService {
//期望此次操作在线程池执行 不会影响原有的主线程
@Async("taskExcutor")//将该任务丢到线程池中
public void updateArticleViewCount(ArticleMapper articleMapper, Article article) {
int viewCounts = article.getViewCounts();
Article articleUpdate = new Article();
articleUpdate.setViewCounts(viewCounts + 1);
LambdaUpdateWrapper<Article> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(Article::getId, article.getId());
//设置一个 为了在多线程的环境下 线程安全
//乐观锁的一个思想 如果操作的时候发现阅读数与期望的阅读数不一致,修改失败
updateWrapper.eq(Article::getViewCounts, viewCounts);
articleMapper.update(articleUpdate, updateWrapper);
try {
//睡眠 ThredService中的方法 5秒,不会影响主线程的使用,即文章详情会很快的显示出来,不受影响
Thread.sleep(5000);
System.out.println("更新完成了~~~");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
统一日志记录、统一缓存处理:
通过自定义注解以及AOP进行统一日志记录和统一缓存处理(需要了解aop)
1.统一缓存处理:通过aop的环绕通知,获取切点的类名、方法名、参数,并通过redis判断是否该缓存存入了 redis。
1.1如果没有存入,就获取注解上标注的过期时间,并存入redis中,设置获取到的过期时间。
1.2如果redis已有缓存,不通过后端接口的执行获得结果,就直接将缓存的结果返回给前端,优化用户等 待的时间
2.统一日志记录(过程类似,可以一笔带过):
目的:如果接口出现了问题,就可以通过日志详情快速定位问题所在
统一缓存处理代码块:
package com.example.blog.common.aop;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {
//设置过期时间
long expire() default 1 * 60 * 1000;
//缓存表示 key
String name() default "";
}
package com.example.blog.common.aop;
import com.alibaba.fastjson.JSON;
import com.example.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.Duration;
@Aspect
@Component
@Slf4j
//缓存的切面(切点为添加了该注解的方法)
public class CacheAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
//加了该注解就成为了一个切点
@Pointcut("@annotation(com.example.blog.common.aop.Cache)")
public void pt() {
}
@Around("pt()")//环绕通知
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
try {
Signature signature = joinPoint.getSignature();
//获取切点的类名
String className = joinPoint.getTarget().getClass().getSimpleName();
//获取切点的方法名
String methodName = signature.getName();
//获取切点的参数
Class[] parameterTypes = new Class[joinPoint.getArgs().length];//存储参数类型的数组
Object[] args = joinPoint.getArgs();//存储参数的数组
String params = "";
for (int i = 0; i < args.length; i++) {
if (args[i] != null) {
params += JSON.toJSONString(args[i]);
parameterTypes[i] = args[i].getClass();
} else {
parameterTypes[i] = null;
}
}
if (StringUtils.isNotEmpty(params)) {
//MD5加密 以防出现key过长以及字符串转义获取不到的情况
params = DigestUtils.md5Hex(params);
}
//通过parameterTypes获得切点
Method method = joinPoint.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);
//获得cache注解
Cache annotation = method.getAnnotation(Cache.class);
//拿到切点设置的缓存过期时间
long expire = annotation.expire();
//拿到切点设置的缓存名称
String name = annotation.name();
//先从redis获得,看是否该缓存存入了redis
String redisKey = name + "::" + className + "::" + methodName + "::" + params;
String redisValue = redisTemplate.opsForValue().get(redisKey);
if (StringUtils.isNotEmpty(redisValue)) {
log.info("走了缓存~~~,{},{}", className, methodName);
return JSON.parseObject(redisValue, Result.class);
}
//如果为空,代表需要调用切点方法,再把返回值存入redis里面
Object proceed = joinPoint.proceed();
redisTemplate.opsForValue().set(redisKey, JSON.toJSONString(proceed), Duration.ofMillis(expire));
log.info("存入缓存~~~ {},{}", className, methodName);
return proceed;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return Result.fail(-999, "系统错误");
}
}
统一日志记录处理代码块:
package com.example.blog.common.aop;
import java.lang.annotation.*;
//ElementType.TYPE代表可以放在类上面;ElementType.METHOD代表可以放在方法上
@Target({ElementType.TYPE, ElementType.METHOD})
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
@Retention(RetentionPolicy.RUNTIME)
//标记注解,用于描述其它类型的注解应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。
@Documented
public @interface LogAnnotation
{
String module() default "";
String operator() default "";
}
package com.example.blog.common.aop;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.api.R;
import com.example.blog.utils.HttpContextUtils;
import com.example.blog.utils.IPUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
@Component
@Aspect //切面(通知+切点)
@Slf4j
public class LogAspect {
//切点就是该注解,这个注解加在哪里,哪里就是切点
@Pointcut("@annotation(com.example.blog.common.aop.LogAnnotation)")
public void pt() {
}
//环绕通知
@Around("pt()")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
long beginTime = System.currentTimeMillis();
//执行方法
Object result = joinPoint.proceed();
//执行时常
Long time = System.currentTimeMillis() - beginTime;
//保存在日志里
recordLog(joinPoint, time);
return result;
}
private void recordLog(ProceedingJoinPoint joinPoint, long time) {
//通过连接点可以获得MethodSignature,以及对应切点方法的详情(方法名、参数)
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获得该切点方法
Method method = signature.getMethod();
//通过该方法获得该注解详情
LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
log.info("=====================log start================================");
log.info("module:{}", logAnnotation.module());
log.info("operation:{}", logAnnotation.operator());
//请求的类名以及方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
log.info("request method:{}", className + "." + methodName + "()");
//请求的参数
Object[] args = joinPoint.getArgs();
String params = JSON.toJSONString(args[0]);
log.info("params:{}", params);
//获取request 设置IP地址
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
log.info("ip:{}", IPUtils.getIpAddr(request));
log.info("execute time : {} ms", time);
log.info("=====================log end================================");
}
}