Codingmore学习

Codingmore学习

项目地址

Codingmore项目结构

  • codingmore-admin,后台管理系统接口

  • codingmore-common,工具类及通用代码

  • codingmore-mbg, MyBatis-Plus 生成的数据库操作代码

  • codingmore-security:SpringSecurity封装公用模块

  • codingmore-web:前台展示系统接口

Codingmore-admin

codingmore-admin
    ├─component
    ├─config
    ├─controller
    └─service
    	└─impl 
  • component
    • PublishPostJob 定时发布文章
  • config
    • CodingmoreSecurityConfig 该Java类定义了两个Bean:userDetailsService和dynamicSecurityService。
      • userDetailsService:根据用户名加载用户的详细信息,用于认证。
      • dynamicSecurityService:从数据库中获取资源列表,并将其转换为安全配置属性,用于授权访问控制。具体来说,它将每个资源的URL与对应的权限关联起来,存储在一个并发哈希映射中。
    • GlobalCorsConfig 全局跨域配置
    • MybatisConfig 用于配置MyBatis相关功能,具体功能如下:
      • 通过@Configuration@EnableTransactionManagement注解声明此为Spring配置类,并开启事务管理。使用@MapperScan注解指定MyBatis映射接口的位置。
      • 定义了mybatisPlusInterceptor方法,创建并返回一个MybatisPlusInterceptor实例,用于添加分页拦截器PaginationInnerInterceptor,确保在MySQL数据库环境下支持分页查询。
    • OssCientConfig 定义了一个配置类,用于创建阿里云OSS客户端。功能如下:
      • 通过@Value注解从配置文件中读取OSS服务的endpointaccessKeyIdaccessKeySecret。使用@Bean注解创建并返回一个OSSClient实例,以便应用程序使用该客户端与阿里云OSS服务交互。
    • SwaggerConfig 配置了Swagger文档,主要功能如下:
      • 定义一个Docket类型的bean,指定API文档的生成规则、启用状态及安全方案。设置API的基本信息,如标题、描述、联系人和版本号。配置安全方案,定义请求头中的认证信息。指定需要认证的API路径。设置默认认证范围。
    • ThreadPoolConfig 定义了一个线程池配置。具体功能如下:
      • 从配置属性中读取线程池的核心参数:核心线程数、最大线程数、队列容量等。
      • 定义一个名为ossUploadImageExecutor的线程池执行器,并设置上述参数。
      • 配置拒绝策略为CallerRunsPolicy,即当任务队列满时,调用者会执行该任务。
      • 设置线程池关闭时等待所有任务完成。
    • WebConfigBeans 添加了自定义的日期转换器
  • controller
    • controller不和数据库中的表一一对应,而是与业务对应
    • CommentsController LinksController PostTagRelationController TermRelationshipsController 中不包含业务逻辑
    • UserController CRUD,禁用用户,返回token,刷新token,获取当前登录用户信息,登出,修改密码,获取角色,分配角色
    • MenuController CRUD,返回所有菜单树状图
    • MinIOController 只包含一个上传接口
    • OssController 一个上传接口
    • PostsController CRUD, 添加文章栏目关联,设置和取消置顶,上传
    • PostTagController CRUD, 模糊匹配,分页查询
    • ResourceCategoryController CRUD
    • ResourceController CRUD
    • RoleController CRUD,分页,获取或分配角色相关的菜单、资源
    • SiteController CRUD
    • TermTaxonomyController 栏目CRUD,查找子栏目
  • service
    • 包含数据库16个表的Service接口和Oss服务的接口
    • Impl 包含所有接口的实现类

Codingmore-common

codingmore-common
    ├─assist
    ├─component
    ├─exception
    ├─state
    ├─util
    └─webapi

codingmore-common

  • assist :

    • RedisConstants类为Java应用程序提供了一组常量和方法,用于处理Redis数据库中的网页浏览量、帖子点赞数以及管理员用户和资源信息。
  • component

    • BindingResultAspect 这段代码是一个使用Spring AOP(面向切面编程)的切面类,用于处理Hibernate Validator(一个Java验证框架)的错误结果。
    • DateConverter 日期转换类,将标准日期、标准日期时间、时间戳转换成Date类型
    • WebLog Controller层的日志封装类,包含了日志的属性
    • WebLogAspect 统一日志处理切面,记录请求信息,包括请求的URL、方法、参数、结果等。
  • exception

    • ApiException 继承 RuntimeException
    • Asserts 断言处理,用于抛出API异常
    • GlobalExceptionHandler 全局异常处理
  • state

    • 包含一些枚举类
    • PostStatus UserStatus 包含文章和用户状态
    • PostType TermRelationType UserType 用于区别类型
  • util

    • FileNameUtil 用于生成文件名称,主要是图片名称,用于OSS服务和MinIO
  • webapi

    • IErrorCode 接口类,封装API的错误码
    • ResultCode IErrorCode的实现类,包含可能用到的操作码
    • ResultObject 通用的返回对象类

全局异常处理

逻辑:参数校验出现异常时抛出,进行捕获

  1. 新建一个自定义异常类 ApiException
  2. 新建一个断言处理类Asserts,失败时抛出ApiException异常
  3. 新建一个全局异常处理类GlobalExceptionHandler, 当捕获到ApiException时,检查异常对象中的errorCode是否为空。如果errorCode不为空,则使用它构建失败的ResultObject返回。
    否则,使用异常消息构建失败的ResultObject返回。

Codingmore-mbg

  • dto :数据传输对象
  • vo : 封装数据便于在视图中展示
  • service : 包含了Redis操作的接口和实现。数据库表对应的Service不在此模块中。
  • mapper :映射接口,定义了与数据库交互的接口。
  • model : 相当于entity,其中每一个类对应一个数据库中的表。
  • config :RedisConfig redis配置类。
  • util : 代码生成工具,日期工具。

Codingmore-security

Codingmore后台登录认证

使用SpringSecurity + JWT,将代码封装在codingmore-security中

codingmore-security
├── component
|    ├── JwtAuthenticationTokenFilter -- JWT登录授权过滤器
|    ├── RestAuthenticationEntryPoint
|    └── RestfulAccessDeniedHandler
├── config
|    ├── IgnoreUrlsConfig
|    └── SecurityConfig
└── util
     └── JwtTokenUtil -- JWT的token处理工具类
  • JwtAuthenticationTokenFilter

    • JWT登录授权过滤器,每次有请求时,从请求头中获取JWT。

    • 从JWT中解析出username,并验证JWT的时效。

    • 校验成功,将token放在ThreadLocal中

    • public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
          private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
          @Autowired
          private UserDetailsService userDetailsService;
          @Autowired
          private JwtTokenUtil jwtTokenUtil;
          @Value("${jwt.tokenHeader}")
          private String tokenHeader;
          @Value("${jwt.tokenHead}")
          private String tokenHead;
      
          @Override
          protected void doFilterInternal(HttpServletRequest request,
                                          HttpServletResponse response,
                                          FilterChain chain) throws ServletException, IOException {
              // 从客户端请求中获取 JWT
              String authHeader = request.getHeader(this.tokenHeader);
              // 该 JWT 是我们规定的格式,以 tokenHead 开头
              if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
                  // The part after "Bearer "
                  String authToken = authHeader.substring(this.tokenHead.length());
                  // 从 JWT 中获取用户名
                  String username = jwtTokenUtil.getUserNameFromToken(authToken);
                  LOGGER.info("checking username:{}", username);
      
                  // SecurityContextHolder 是 SpringSecurity 的一个工具类
                  // 保存应用程序中当前使用人的安全上下文
                  if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                      // 根据用户名获取登录用户信息
                      UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                      // 验证 token 是否过期
                      if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                          // 将登录用户保存到安全上下文中
                          UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,
                                  null, userDetails.getAuthorities());
                          authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                          SecurityContextHolder.getContext().setAuthentication(authentication);
      
                          LOGGER.info("authenticated user:{}", username);
                      }
                  }
              }
              chain.doFilter(request, response);
          }
      }
      
  • JWTtokenUtil

    • JWTtoken的工具类

    • /**
       * JwtToken生成的工具类
       * JWT token的格式:header.payload.signature
       * header的格式(算法、token的类型):
       * {"alg": "HS512","typ": "JWT"}
       * payload的格式(用户名、创建时间、生成时间):
       * {"sub":"wang","created":1489079981393,"exp":1489684781}
       * signature的生成算法:
       * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
       *    on 2018/4/26.
       */
      public class JwtTokenUtil {
          private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
      
          private static final String CLAIM_KEY_USERNAME = "sub";
          private static final String CLAIM_KEY_CREATED = "created";
      
          @Value("${jwt.secret}")
          private String secret;
          @Value("${jwt.expiration}")
          private Long expiration;
          @Value("${jwt.tokenHead}")
          private String tokenHead;
      
          /**
           * 根据用户名、创建时间生成JWT的token
           */
          private String generateToken(Map<String, Object> claims) {
              return Jwts.builder()
                      .setClaims(claims)
                      .setExpiration(generateExpirationDate())
                      .signWith(SignatureAlgorithm.HS512, secret)
                      .compact();
          }
      
          /**
           * 从token中获取JWT中的负载
           */
          private Claims getClaimsFromToken(String token) {
              Claims claims = null;
              try {
                  claims = Jwts.parser()
                          .setSigningKey(secret)
                          .parseClaimsJws(token)
                          .getBody();
              } catch (Exception e) {
                  LOGGER.info("JWT格式验证失败:{}", token);
              }
              return claims;
          }
      
          /**
           * 生成token的过期时间
           */
          private Date generateExpirationDate() {
              return new Date(System.currentTimeMillis() + expiration * 1000);
          }
      
          /**
           * 从token中获取登录用户名
           */
          public String getUserNameFromToken(String token) {
              String username = null;
              Claims claims = getClaimsFromToken(token);
              if (claims != null) {
                  username = claims.getSubject();
              }
      
              return username;
          }
      
          /**
           * 验证token是否还有效
           *
           * @param token       客户端传入的token
           * @param userDetails 从数据库中查询出来的用户信息
           */
          public boolean validateToken(String token, UserDetails userDetails) {
              String username = getUserNameFromToken(token);
              return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
          }
      
          /**
           * 判断token是否已经失效
           */
          private boolean isTokenExpired(String token) {
              Date expiredDate = getExpiredDateFromToken(token);
              return expiredDate.before(new Date());
          }
      
          /**
           * 从token中获取过期时间
           */
          private Date getExpiredDateFromToken(String token) {
              Claims claims = getClaimsFromToken(token);
              return claims.getExpiration();
          }
      
          /**
           * 根据用户信息生成token
           */
          public String generateToken(UserDetails userDetails) {
              Map<String, Object> claims = new HashMap<>();
              claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
              claims.put(CLAIM_KEY_CREATED, new Date());
              return generateToken(claims);
          }
      
          /**
           * 当原来的token没过期时是可以刷新的
           *
           * @param oldToken 带tokenHead的token
           */
          public String refreshHeadToken(String oldToken) {
              if(StrUtil.isEmpty(oldToken)){
                  return null;
              }
              String token = oldToken.substring(tokenHead.length());
              if(StrUtil.isEmpty(token)){
                  return null;
              }
              //token校验不通过
              Claims claims = getClaimsFromToken(token);
              if(claims==null){
                  return null;
              }
              //如果token已经过期,不支持刷新
              if(isTokenExpired(token)){
                  return null;
              }
              //如果token在30分钟之内刚刷新过,返回原token
              if(tokenRefreshJustBefore(token,30*60)){
                  return token;
              }else{
                  claims.put(CLAIM_KEY_CREATED, new Date());
                  return generateToken(claims);
              }
          }
      
          /**
           * 判断token在指定时间内是否刚刚刷新过
           * @param token 原token
           * @param time 指定时间(秒)
           */
          private boolean tokenRefreshJustBefore(String token, int time) {
              Claims claims = getClaimsFromToken(token);
              Date created = claims.get(CLAIM_KEY_CREATED, Date.class);
              Date refreshDate = new Date();
              //刷新时间在创建时间的指定时间内
              if(refreshDate.after(created)&&refreshDate.before(DateUtil.offsetSecond(created,time))){
                  return true;
              }
              return false;
          }
      }
      
  • RestAuthenticationEntryPoint

    • 自定义返回结果,未登录或者过期

    • public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
          @Override
          public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
              response.setHeader("Access-Control-Allow-Origin", "*");
              response.setHeader("Cache-Control","no-cache");
              response.setCharacterEncoding("UTF-8");
              response.setContentType("application/json");
              response.getWriter().println(JSONUtil.parse(ResultObject.unauthorized(authException.getMessage())));
              response.getWriter().flush();
          }
      }
      
  • RestfulAccessDeniedHandler

    • 自定义返回结果,没有权限访问时

    • public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
          @Override
          public void handle(HttpServletRequest request,
                             HttpServletResponse response,
                             AccessDeniedException e) throws IOException, ServletException {
              response.setHeader("Access-Control-Allow-Origin", "*");
              response.setHeader("Cache-Control","no-cache");
              response.setCharacterEncoding("UTF-8");
              response.setContentType("application/json");
              response.getWriter().println(JSONUtil.parse(ResultObject.forbidden(e.getMessage())));
              response.getWriter().flush();
          }
      }
      
  • IgnoreUrlsConfig

    • 配置不需要安全保护的资源

    • @Getter
      @Setter
      @ConfigurationProperties(prefix = "secure.ignored")
      public class IgnoreUrlsConfig {
      
          private List<String> urls = new ArrayList<>();
      
      }
      
  • SecurityConfig

    • SpringSecurity通用配置

    • public class CustomSecurityConfig extends WebSecurityConfigurerAdapter {
      
          @Autowired(required = false)
          private DynamicSecurityService dynamicSecurityService;
      
          @Override
          protected void configure(HttpSecurity httpSecurity) throws Exception {
              ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
                      .authorizeRequests();
              //允许跨域请求的OPTIONS请求
              registry.antMatchers(HttpMethod.OPTIONS)
                      .permitAll();
      
              //不需要保护的资源路径允许访问
              for (String url : ignoreUrlsConfig().getUrls()) {
                  registry.antMatchers(url).permitAll();
              }
      
              // 任何请求需要身份认证
              registry.and()
                      .authorizeRequests()
                      .anyRequest()
                      .authenticated()
                      // 关闭跨站请求防护及不使用session
                      .and()
                      .csrf()
                      .disable()
                      .sessionManagement()
                      .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                      // 自定义权限拒绝处理类
                      .and()
                      .exceptionHandling()
                      .accessDeniedHandler(restfulAccessDeniedHandler())
                      .authenticationEntryPoint(restAuthenticationEntryPoint())
                      // 自定义权限拦截器JWT过滤器
                      .and()
                      .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
              //有动态权限配置时添加动态权限校验过滤器
              if(dynamicSecurityService!=null){
                  registry.and().addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class);
              }
          }
      
          @Override
          protected void configure(AuthenticationManagerBuilder auth) throws Exception {
              auth.userDetailsService(userDetailsService())
                      .passwordEncoder(passwordEncoder());
          }
      
          @Bean
          public PasswordEncoder passwordEncoder() {
              return new BCryptPasswordEncoder();
          }
      
          @Bean
          public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
              return new JwtAuthenticationTokenFilter();
          }
      
          @Bean
          @Override
          public AuthenticationManager authenticationManagerBean() throws Exception {
              return super.authenticationManagerBean();
          }
      
          @Bean
          public RestfulAccessDeniedHandler restfulAccessDeniedHandler() {
              return new RestfulAccessDeniedHandler();
          }
      
          @Bean
          public RestAuthenticationEntryPoint restAuthenticationEntryPoint() {
              return new RestAuthenticationEntryPoint();
          }
      
          @Bean
          public IgnoreUrlsConfig ignoreUrlsConfig() {
              return new IgnoreUrlsConfig();
          }
      
          @Bean
          public JwtTokenUtil jwtTokenUtil() {
              return new JwtTokenUtil();
          }
      
          @ConditionalOnBean(name = "dynamicSecurityService")
          @Bean
          public DynamicAccessDecisionManager dynamicAccessDecisionManager() {
              return new DynamicAccessDecisionManager();
          }
      
      
          @ConditionalOnBean(name = "dynamicSecurityService")
          @Bean
          public DynamicSecurityFilter dynamicSecurityFilter() {
              return new DynamicSecurityFilter();
          }
      
          @ConditionalOnBean(name = "dynamicSecurityService")
          @Bean
          public DynamicSecurityMetadataSource dynamicSecurityMetadataSource() {
              return new DynamicSecurityMetadataSource();
          }
      
      }
      

Codingmore动态权限配置

不同的角色拥有不同的权限,需要对他们被允许进行的操作进行区分。

数据库划分:

user role admin_role_relation

role resource role_resource_relation

用户与角色关联,角色对应一组允许操作的资源。

首先,配置路径与资源的对应关系。通过实现DynamicSecurityService中的loadDataSource()方法,将需要资源的路径与资源一一对应。该接口的实现类位于condingmore-admin的CodingmoreSecurityConfig中配置。

配置完成后,第一步需要一个动态权限过滤器,用于实现基于路径的动态权限过滤。

其中,OPTIONS请求放行,白名单请求放行,需要鉴权的请求调用AccessDecisionManager中的decide()方法进行鉴权操作。

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) servletRequest;
    FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
    //OPTIONS请求直接放行
    if(request.getMethod().equals(HttpMethod.OPTIONS.toString())){
        fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        return;
    }
    //白名单请求直接放行
    PathMatcher pathMatcher = new AntPathMatcher();
    for (String path : ignoreUrlsConfig.getUrls()) {
        if(pathMatcher.match(path,request.getRequestURI())){
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            return;
        }
    }
    //此处会调用AccessDecisionManager中的decide方法进行鉴权操作
    InterceptorStatusToken token = super.beforeInvocation(fi);
    try {
        fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
    } finally {
        super.afterInvocation(token, null);
    }
}

实现动态权限决策管理器,判断用户是否有访问权限。decide()方法中将接口需要的资源与用户拥有的资源进行对比,用户拥有权限直接返回,没有权限抛出AccessDeniedException异常。

decide()方法如下

@Override
public void decide(Authentication authentication, Object object,
                   Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
    // 当接口未被配置资源时直接放行
    if (CollUtil.isEmpty(configAttributes)) {
        return;
    }
    Iterator<ConfigAttribute> iterator = configAttributes.iterator();
    while (iterator.hasNext()) {
        ConfigAttribute configAttribute = iterator.next();
        //将访问所需资源或用户拥有资源进行比对
        String needAuthority = configAttribute.getAttribute();
        for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
            if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
                return;
            }
        }
    }
    throw new AccessDeniedException("抱歉,您没有访问权限");
}

DynamicSecurityFilter中调用super.beforeInvocation(fi)方法时会调用AccessDecisionManager中的decide方法用于鉴权操作,而decide方法中的configAttributes参数会通过SecurityMetadataSource中的getAttributes方法来获取,configAttributes其实就是配置好的访问当前接口所需要的权限。

getAttributes()方法如下:

@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
    if (configAttributeMap == null) this.loadDataSource();
    List<ConfigAttribute>  configAttributes = new ArrayList<>();
    //获取当前访问的路径
    String url = ((FilterInvocation) o).getRequestUrl();
    String path = URLUtil.getPath(url);
    PathMatcher pathMatcher = new AntPathMatcher();
    Iterator<String> iterator = configAttributeMap.keySet().iterator();
    //获取访问该路径所需资源
    while (iterator.hasNext()) {
        String pattern = iterator.next();
        if (pathMatcher.match(pattern, path)) {
            configAttributes.add(configAttributeMap.get(pattern));
        }
    }
    // 未设置操作请求权限,返回空集合
    return configAttributes;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Liwan95

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值
>