12.总结-技术亮点

8 篇文章 0 订阅

前后端所用技术栈: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================================");
    }
}
​

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值