SpringBoot学习项目-博客系统-part9

  • 接下来就对接口的开发不做完整的记录,只是说明几点
    1. 开发接口的时候根据文档看清参数、地址、请求方式。
    2. 参数如果不是一个实体类的,建议进行封装
    3. 根据前端返回数据格式确定在serviceImpl最终返回的数据形式。具体是一个实体类还是封装的类对象
    4. 因为mp无法进行多表联合查询,当涉及的时候,使用xml或者直接在Mapper中写sql语句。

统一缓存处理(优化)

  1. 内存的访问速度远远大于磁盘的访问速度(1000倍起)
  2. 继续用AOP开发缓存功能,缓存加在哪个地方哪个地方就是切点
// 切点
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {

    long expire() default 1 * 60 * 1000;
    //缓存标识 key
    String name() default "";

}
  1. 定义切面,切面定义了切点和通知的关系
//aop 定义一个切面,切面定义了切点和通知的关系
@Aspect
@Component
@Slf4j
public class CacheAspect {

    private static final ObjectMapper objectMapper = new ObjectMapper();


    @Autowired
    private RedisTemplate<String, String> redisTemplate;
//  切点
    @Pointcut("@annotation(com.lbj.blog.common.cache.Cache)")
    public void pt(){}
//  环绕通知
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp){
        try {
            Signature signature = pjp.getSignature();
            //类名
            String className = pjp.getTarget().getClass().getSimpleName();
            //调用的方法名
            String methodName = signature.getName();


            Class[] parameterTypes = new Class[pjp.getArgs().length];
            Object[] args = pjp.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)) {
                //加密 以防出现key过长以及字符转义获取不到的情况
                params = DigestUtils.md5Hex(params);
            }
//            获取方法
            Method method = pjp.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);
            //获取Cache注解
            Cache annotation = method.getAnnotation(Cache.class);
            //缓存过期时间
            long expire = annotation.expire();
            //缓存名称
            String name = annotation.name();
            //先从redis获取
            String redisKey = name + "::" + className+"::"+methodName+"::"+params;
            String redisValue = redisTemplate.opsForValue().get(redisKey);
            if (StringUtils.isNotEmpty(redisValue)){
                log.info("走了缓存~~~,{},{}",className,methodName);
                Result result = JSON.parseObject(redisValue, Result.class);
                return result;
            }
//            访问方法
            Object proceed = pjp.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,"系统错误");
    }

}

controller加上切点

/**
     * 首页 最热文章
     * @return
     */
    @PostMapping("hot")
    @Cache(expire = 5 * 60 * 1000,name = "hot_article")
    public Result hotArticle(){
        int limit = 5;
        return articleService.hotArticle(limit);
    }

权限部分

  1. 先说一下数据库表

ms_admin(权限用户表)

id                权限用户的id,主键
username          权限用户的用户名
password          权限用户的密码     

ms_permission(权限表名)

id                权限表的id,主键
name              权限名
path              权限路径
description       权限的描述

ms_admin_permission(权限用户对应权限表的关系表)

id                关系表id,主键
admin_id          权限用户的id
permission_id     权限id

权限管理

controller

  1. 接口中有权限的列表、增删改
@RestController
@RequestMapping("admin")
public class AdminController {

    @Autowired
    private PermissionService permissionService;

    @PostMapping("permission/permissionList")
    public Result permissionList(@RequestBody PageParam pageParam){
        return permissionService.listPermission(pageParam);
    }

    @PostMapping("permission/add")
    public Result add(@RequestBody Permission permission){
        return permissionService.add(permission);
    }

    @PostMapping("permission/update")
    public Result update(@RequestBody Permission permission){
        return permissionService.update(permission);
    }

    @GetMapping("permission/delete/{id}")
    public Result delete(@PathVariable("id") Long id){
        return permissionService.delete(id);
    }
}

service

@Service
public class PermissionService {

    @Autowired
    private PermissionMapper permissionMapper;

    public Result listPermission(PageParam pageParam){
        Page<Permission> page = new Page<>(pageParam.getCurrentPage(),pageParam.getPageSize());
        LambdaQueryWrapper<Permission> queryWrapper = new LambdaQueryWrapper<>();
        if (StringUtils.isNotBlank(pageParam.getQueryString())) {
            queryWrapper.eq(Permission::getName,pageParam.getQueryString());
        }
        Page<Permission> permissionPage = this.permissionMapper.selectPage(page, queryWrapper);
        PageResult<Permission> pageResult = new PageResult<>();
        pageResult.setList(permissionPage.getRecords());
        pageResult.setTotal(permissionPage.getTotal());
        return Result.success(pageResult);
    }

    public Result add(Permission permission) {
        this.permissionMapper.insert(permission);
        return Result.success(null);
    }

    public Result update(Permission permission) {
        this.permissionMapper.updateById(permission);
        return Result.success(null);
    }

    public Result delete(Long id) {
        this.permissionMapper.deleteById(id);
        return Result.success(null);
    }
}
  • 因为权限,就需要整合springsecurity
  1. 添加依赖
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  1. 权限配置类
  • 自定义的权限服务配置要实现UserDetailsService接口,主要是实现UserDetailsService方法,加密策略选择了BCryptPasswordEncoder
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    public static void main(String[] args) {
        //加密策略 MD5 不安全 彩虹表  MD5 加盐
        String mszlu = new BCryptPasswordEncoder().encode("mszlu");
        System.out.println(mszlu);
    }
    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests() //开启登录认证
//                .antMatchers("/user/findAll").hasRole("admin") //访问接口需要admin的角色
                .antMatchers("/css/**").permitAll()
                .antMatchers("/img/**").permitAll()
                .antMatchers("/js/**").permitAll()
                .antMatchers("/plugins/**").permitAll()
                .antMatchers("/admin/**").access("@authService.auth(request,authentication)") //自定义service 来去实现实时的权限认证
                .antMatchers("/pages/**").authenticated()
                .and().formLogin()
                .loginPage("/login.html") //自定义的登录页面
                .loginProcessingUrl("/login") //登录处理接口
                .usernameParameter("username") //定义登录时的用户名的key 默认为username
                .passwordParameter("password") //定义登录时的密码key,默认是password
                .defaultSuccessUrl("/pages/main.html")
                .failureUrl("/login.html")
                .permitAll() //通过 不拦截,更加前面配的路径决定,这是指和登录表单相关的接口 都通过
                .and().logout() //退出登录配置
                .logoutUrl("/logout") //退出登录接口
                .logoutSuccessUrl("/login.html")
                .permitAll() //退出登录的接口放行
                .and()
                .httpBasic()
                .and()
                .csrf().disable() //csrf关闭 如果自定义登录 需要关闭
                .headers().frameOptions().sameOrigin();
    }
}

权限认证

  • 因为在配置的时候是使用了自定义serviceauthService进行权限认证的
@Service
@Slf4j
public class AuthService {

    @Autowired
    private AdminService adminService;

    public boolean auth(HttpServletRequest request, Authentication authentication){
        String requestURI = request.getRequestURI();
        log.info("request url:{}", requestURI);
        //true代表放行 false 代表拦截
        Object principal = authentication.getPrincipal();
        if (principal == null || "anonymousUser".equals(principal)){
            //未登录
            return false;
        }
        UserDetails userDetails = (UserDetails) principal;
        String username = userDetails.getUsername();
        Admin admin = adminService.findAdminByUserName(username);
        if (admin == null){
            return false;
        }
        if (admin.getId() == 1){
            //认为是超级管理员
            return true;
        }
        List<Permission> permissions = adminService.findPermissionsByAdminId(admin.getId());
        requestURI = StringUtils.split(requestURI,'?')[0];
        for (Permission permission : permissions) {
            if (requestURI.equals(permission.getPath())){
                log.info("权限通过");
                return true;
            }
        }
        return false;
    }
}

AdminMapper

public interface AdminMapper extends BaseMapper<Admin> {
}

PermissionMapper

public interface PermissionMapper extends BaseMapper<Permission> {

    List<Permission> findPermissionsByAdminId(Long adminId);
}

sql

<mapper namespace="com.mszlu.blog.admin.mapper.PermissionMapper">

    <select id="findPermissionsByAdminId" parameterType="long" resultType="com.mszlu.blog.admin.pojo.Permission">
        select * from ms_permission where id in (select permission_id from ms_admin_permission where admin_id=#{adminId})
    </select>
</mapper>
  • 该项目是一个单体项目,麻雀虽小五脏俱全,虽然简简单单的就总结完成了,总结还是根据当时文档进行总结的,自己也对着视频敲了三五天吧。说一下自己对这个项目的感觉

总结

  1. 任何一个项目给了数据库,在开发对应的接口的时候,应该好好看看对应的数据库表,这样才能进行相应的开发。
  2. 从最开始的依赖包的管理(maven)
  3. 设计到新的orm框架-mybatis-plus,其实这个简单的看一下就可以了,大致和mybatis差不多,但是使用是有分页插件的,所以要使用mp的时候要把mp插件配置写好。
  4. 是一个前后端分离项目,那么前后端分离就需要统一状态返回码
  5. 在开发的时候,永远牢记controller->service->mapper(dao),每个mapper会继承basemapper,但是mp的缺点是无法进行多表联合查询,所以一旦涉及到了多表联合查询,需要自己手写sql语句,这部分就mybatis的
    1. 如果xml是在mapper包下,要注意可能出现绑定异常的问题,这个时候就需要pom文件绑定xml以及在配置文件中配置xml文件路径。
    2. 如果xml在resource下,注意要和包名要一致,此时就不需要再配置和pom绑定了。
  6. 需要统一的异常处理,针对可以预见的异常进行处理
  7. 看前端返回数据形式,以及每次请求的参数,如果不是一个实体类能够解决的,那么就进行封装。
  8. JWT的使用,登陆的时候,密码不能以明文的方式传递到数据库表中,使用md5加盐的方式加密,JWT最关键的就是签名部分,这部分一般会有工具类
  9. 使用到redis,登陆的时候,服务器生成的token保存到redis中,以后可以直接从redis中进行取,速度快一些。
  10. 考虑事务,如果登陆的时候redis挂掉,就不能在数据库中保存注册的账户密码等信息,所以使用到@Transactional注解,这个注解在底层是ThreadLocal
  11. 拦截器,如果访问的是一些静态资源就不需要进行拦截,如果是需要登陆的,那么就需要进行拦截判断,同时放行之后将用户信息放在自己定义个ThreadLocal中,但是在使用了ThreadLocal之后一定要remove,不然就会发生内存泄露问题。之后需要可以直接获取。
    拦截器的配置。(通常也是写好的,但是还是看一下)
  12. 线程池的使用,如何去配置线程池的时候要写注解@EnableAsync(开启多线程),配置线程池的名字@Bean("taskExecutor"),以及在相应要使用线程的地方标注@Async("taskExecutor")
  13. 分布式id在前端会造成精度缺失,使用@JsonSerialize(using = ToStringSerializer.class)
  14. spring的两大特性:IOC和AOP
  15. 开发日志和缓存,都使用到了AOP
  16. 上传图片这些文件可以上传至云服务器上,比如:七牛云、阿里云...
  17. springsecurity的整合,用来进行权限管理,如果自己要自定义的权限登陆要继承WebSecurityConfigurerAdapter,自定义权限服务配置要实现UserDetailsService
上一篇:Android Manifest功能与权限描述大全,阿里后台开发


下一篇:七、权限