项目中适配

Security配置类

创建maven项目:略

配置pom文件

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
  

    <groupId>com.yc</groupId>
    <artifactId>server</artifactId>
    <version>0.0.1-SNAPSHOT</version>


    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <!--web 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--lombok 依赖-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--mysql 依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--mybatis-plus 依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.1.tmp</version>
        </dependency>
        <!-- swagger2 依赖 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.7.0</version>
        </dependency>
        <!-- Swagger第三方ui依赖 -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.9.6</version>
        </dependency>
        <!--    &lt;!&ndash;security 依赖&ndash;&gt;-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--    &lt;!&ndash;JWT 依赖&ndash;&gt;-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <!--    &lt;!&ndash; google kaptcha依赖 &ndash;&gt;-->
        <dependency>
            <groupId>com.github.axet</groupId>
            <artifactId>kaptcha</artifactId>
            <version>0.0.9</version>
        </dependency>
        <!--    &lt;!&ndash; spring data redis 依赖 &ndash;&gt;-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--    &lt;!&ndash; commons-pool2 对象池依赖 &ndash;&gt;-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!--    &lt;!&ndash;easy poi依赖&ndash;&gt;-->
        <dependency>
            <groupId>cn.afterturn</groupId>
            <artifactId>easypoi-spring-boot-starter</artifactId>
            <version>4.1.3</version>
        </dependency>
        <!--rabbitmq 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!--websocket 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <!--FastDFS依赖-->
        <!--            <dependency>-->
        <!--              <groupId>org.csource</groupId>-->
        <!--              <artifactId>fastdfs-client-java</artifactId>-->
        <!--            </dependency>-->
        <!--FastDFS依赖-->
        <dependency>
            <groupId>org.csource</groupId>
            <artifactId>fastdfs-client-java</artifactId>
            <version>1.29-SNAPSHOT</version>
        </dependency>
        <!--thymeleaf 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>
    <!--  <dependencies>-->

    <!--    &lt;!&ndash;web 依赖&ndash;&gt;-->
    <!--    <dependency>-->
    <!--      <groupId>org.springframework.boot</groupId>-->
    <!--      <artifactId>spring-boot-starter-web</artifactId>-->
    <!--    </dependency>-->
    <!--    &lt;!&ndash;lombok 依赖&ndash;&gt;-->
    <!--    <dependency>-->
    <!--      <groupId>org.projectlombok</groupId>-->
    <!--      <artifactId>lombok</artifactId>-->
    <!--    </dependency>-->

    <!--    &lt;!&ndash;mysql 依赖&ndash;&gt;-->
    <!--    <dependency>-->
    <!--      <groupId>mysql</groupId>-->
    <!--      <artifactId>mysql-connector-java</artifactId>-->
    <!--      <scope>runtime</scope>-->
    <!--    </dependency>-->

    <!--    &lt;!&ndash;mybatis-plus 依赖&ndash;&gt;-->
    <!--    <dependency>-->
    <!--      <groupId>com.baomidou</groupId>-->
    <!--      <artifactId>mybatis-plus-boot-starter</artifactId>-->
    <!--    </dependency>-->
    <!--    <dependency>-->
    <!--      <groupId>org.springframework.boot</groupId>-->
    <!--      <artifactId>spring-boot-autoconfigure</artifactId>-->
    <!--      <version>2.7.5</version>-->
    <!--    </dependency>-->
    <!--    &lt;!&ndash; swagger2 依赖 &ndash;&gt;-->
    <!--&lt;!&ndash;    <dependency>&ndash;&gt;-->
    <!--&lt;!&ndash;      <groupId>io.springfox</groupId>&ndash;&gt;-->
    <!--&lt;!&ndash;      <artifactId>springfox-swagger2</artifactId>&ndash;&gt;-->
    <!--&lt;!&ndash;      <version>2.7.0</version>&ndash;&gt;-->
    <!--&lt;!&ndash;    </dependency>&ndash;&gt;-->
    <!--    &lt;!&ndash; Swagger第三方ui依赖 &ndash;&gt;-->
    <!--    &lt;!&ndash;        <dependency>&ndash;&gt;-->
    <!--    &lt;!&ndash;            <groupId>com.github.xiaoymin</groupId>&ndash;&gt;-->
    <!--    &lt;!&ndash;            <artifactId>swagger-bootstrap-ui</artifactId>&ndash;&gt;-->
    <!--    &lt;!&ndash;        </dependency>&ndash;&gt;-->
    <!--&lt;!&ndash;    <dependency>&ndash;&gt;-->
    <!--&lt;!&ndash;      <groupId>com.github.xiaoymin</groupId>&ndash;&gt;-->
    <!--&lt;!&ndash;      <artifactId>knife4j-spring-ui</artifactId>&ndash;&gt;-->
    <!--&lt;!&ndash;    </dependency>&ndash;&gt;-->
    <!--  </dependencies>-->

</project>

添加依赖

server:
  port: xxxx
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/xxx?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
    username: xxxx
    password: xxxx
    type: com.mysql.cj.jdbc.MysqlDataSource
    hikari:
      # 连接池名
      pool-name: DateHikariCP
      # 最小空闲连接数
      minimum-idle: 5
      # 空闲连接存活最大时间,默认60000010分钟)
      idle-timeout: 180000
      # 最大连接数,默认10
      maximum-pool-size: 10
      # 从连接池返回的连接的自动提交
      auto-commit: true
      # 连接最大存活时间,0表示永久存活,默认180000030分钟)
      max-lifetime: 1800000
      # 连接超时时间,默认3000030秒)
      connection-timeout: 30000
      # 测试连接是否可用的查询语句
      connection-test-query: SELECT 1

  #redis配置
  redis:
    timeout: 10000ms        #连接超时时间
    host: xxx.x.x.x         #redis服务器地址
    port: xxxx              #redis服务器端口
    database: 0             #选择那个库,默认0库
    lettuce:
      pool:
        max-active: 1024    #最大连接数,默认8
        max-wait: 10000ms   #最大连接阻塞等待时间,单位毫秒,默认-1
        max-idle: 200       #最大空闲连接,默认8
        min-idle: 5         #最小空闲连接,默认0

  # rabbitmq配置
  rabbitmq:
    # 用户名
    username: xxxx
    # 密码
    password: xxxx
    # 服务器地址
    host: xxx.xxx.xxx.xxx
    # 端口
    port: xxxx
    # 消息失败回调
    publisher-returns: true
    # 消息确认回调
    publisher-confirm-type: correlated
  # 用户名
# Mybatis-plus配置
mybatis-plus:
  # 配置Mapper映射文件
  mapper-locations: classpath*:/mapper/*.xml 
  # 配置MyBatis数据返回类型别名(默认别名是类名)
  type-aliases-package: com.yc.server.pojo
  configuration:
    # 自动驼峰命名
    map-underscore-to-camel-case: false
## Mybatis SQL 打印(方法接口所在的包,不是Mapper.xml所在的包)
logging:
  level:
    com.yc.server.mapper: debug

jwt:
  # JWT存储的请求头
  tokenHeader: Authorization
  # JWT 加解密使用的密钥
  secret: xxx
  # JWT的超期限时间(60*60*24)
  expiration: 604800
  # JWT 负载中拿到开头
  tokenHead: xxx

JWT工具类

package com.xx.server.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JWT Token工具类
 *
 * @Author xx
 * @PackageName yeb
 * @Package com.xx.server.security
 * @Date 2023/2/3 16:05
 */
@Component
public class JWTTokenUtil {

    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;

    /**
     * 根据负载生产JWT Token
     *
     * @param claims
     * @return
     */
    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpiraationDate())
                .signWith(SignatureAlgorithm.HS256, secret)
                .compact();
    }

    /**
     * 从token中获取JWT中的负载
     * @param token
     * @return
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return claims;
    }

    /**
     * 生产token过期时间
     *
     * @return
     */
    private Date generateExpiraationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 从token中获取过期时间
     * @param token
     * @return
     */
    private Date getExpiredDateFromToken(String token){
        Claims claims=getClaimsFromToken(token);
        return claims.getExpiration();
    }

    /**
     * 判读token是是否失效
     * @param token
     * @return
     */
    private boolean isTokenExprired(String token){
        Date expiredDate=getExpiredDateFromToken(token);
        return expiredDate.before(new Date());
    }

    /**
     * 从token中获取登录用户名
     * @param token
     * @return
     */
    public String getUserNameFormToken(String token){
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username =claims.getSubject();
        } catch (Exception e) {
          e.printStackTrace();
          username=null;
        }
        return username;
    }

    /**
     * 验证token是否有效
     * @param token
     * @param userDetails
     * @return
     */
    public boolean validateToken(String token , UserDetails userDetails){
        String username=getUserNameFormToken(token);
        return username.equals(userDetails.getUsername())&&!isTokenExprired(token);
    }

    /**
     * 根据用户信息生产token
     * @param userDetails
     * @return
     */
    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 token
     * @return
     */
    public boolean canRefresh(String token){
        return !isTokenExprired(token);
    }

    /**
     * 刷新token
     * @param token
     * @return
     */
    public String refreshToken(String token){
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED,new Date());
        return generateToken(claims);
    }
   /**
     * 设置token过期时间为当前时间之前
     * @param token
     * @return
     */
    public String invalidateToken(String token) {
        Claims claims = getClaimsFromToken(token);
        if (claims != null) {
            // 设置过期时间为当前时间之前
            claims.setExpiration(new Date(System.currentTimeMillis() - 1000));
            return generateToken(claims);
        } else {
            return null;
        }
    }

}

用户实现UserDetails

添加公共返回对象

package com.yc.server.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 通用返回结果对象
 * @Author xx
 * @PackageName xxx
 * @Package com.xx.server.pojo
 * @Date 2023/2/3 16:25
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespBean {

    private long code;
    private String message;
    private Object obj;

    /**
     * 成功返回结果
     * @param message
     * @return
     */
    public static RespBean success(String message){
        return new RespBean(200,message,null);
    }

    /**
     * 成功返回结果
     * @param message
     * @param obj
     * @return
     */
    public static RespBean success(String message,Object obj){
        return new RespBean(200,message,obj);
    }

    /**
     * 失败返回结果
     * @param message
     * @return
     */
    public static RespBean error(String message){
        return new RespBean(500,message,null);
    }

    /**
     * 失败返回结果
     * @param message
     * @param obj
     * @return
     */
    public static RespBean error(String message,Object obj){
        return new RespBean(500,message,obj);
    }




}

添加登录相关接口

package com.yc.server.pojo;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

/**
 * 登录接口对象
 * @Author xx
 * @PackageName xxx
 * @Package com.xx.server.pojo
 * @Date 2023/2/3 16:30
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "AdminLogin对象",description = "")
public class AdminLoginParam {
    @ApiModelProperty(value = "用户名",required = true)
    private String username;
    @ApiModelProperty(value = "密码",required = true)
    private String password;
    @ApiModelProperty(value = "密验证码",required = true)
    private String code;

}

package com.xx.server.controller;

import com.xx.server.pojo.Admin;
import com.xx.server.pojo.AdminLoginParam;
import com.xx.server.pojo.RespBean;
import com.yc.server.service.IAdminService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.security.Principal;

/**
 * 登录控制器
 * @Author xx
 * @PackageName xxx
 * @Package com.xx.server.controller
 * @Date 2023/2/3 16:33
 */
@Api(tags = "LoginController")
@RestController
public class LoginController {

    @Autowired
    private IAdminService adminService;

    @ApiOperation(value = "登陆之后返回token")
    @PostMapping("/login")
    public RespBean login(@RequestBody AdminLoginParam adminLoginParam, HttpServletRequest request){
        return adminService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),
                adminLoginParam.getCode(),request);
    }

    @ApiOperation(value = "获取当前用户信息")
    @GetMapping("/admin/info")
    public Admin getAdminInfo(Principal principal){
        if (null == principal){
            return null;
        }
        String username = principal.getName();
        Admin admin = adminService.getAdminByUserName(username);
        admin.setPassword(null);
//        admin.setRoles(adminService.getRoles(admin.getId()));
        return admin;
    }

    @ApiOperation(value = "退出登录")
    @PostMapping("/logout")
    public RespBean logout(){
        return RespBean.success("注销成功!");
    }

}

package com.xx.server.service;

import com.xx.server.pojo.Admin;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xx.server.pojo.RespBean;

import javax.servlet.http.HttpServletRequest;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author xx
 * @since 2023-06-28
 */
public interface IAdminService extends IService<Admin> {

    /**
     * 登录返回token
     *
     * @param username
     * @param password
     * @param request
     * @return
     */
    RespBean login(String username, String password, String code, HttpServletRequest request);

    /**
     * 根据用户名获取用户
     *
     * @param username
     * @return
     */
    Admin getAdminByUserName(String username);

}

package com.xx.server.service.impl;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xx.server.mapper.AdminMapper;
import com.xx.server.mapper.AdminRoleMapper;
import com.xx.server.mapper.RoleMapper;
import com.xx.server.pojo.*;
import com.xx.server.config.security.JWTTokenUtil;
import com.xx.server.service.IAdminService;
import com.xx.server.utils.AdminUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import javax.servlet.http.HttpServletRequest;
import java.sql.Struct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author xx
 * @since 2023-02-01
 */
@Service
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService {

    @Autowired
    private AdminMapper adminMapper;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private RoleMapper roleMapper;
    @Autowired
    private AdminRoleMapper adminRoleMapper;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private JWTTokenUtil jwtTokenUtil;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    /**
     * 登录返回token
     *
     * @param username
     * @param password
     * @param code
     * @param request
     * @return
     */
    @Override
    public RespBean login(String username, String password, String code, HttpServletRequest request) {
        //1.通过用户名查询用户信息 判断用户名是否存在,账号是否被禁用
        String captcha = (String) request.getSession().getAttribute("captcha");
        if (StringUtils.isBlank(code) || !captcha.equals(code)) {
            return RespBean.error("验证码填写错误");
        }
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (null == userDetails || !passwordEncoder.matches(password, userDetails.getPassword())) {
            return RespBean.error("用户名或密码不正确!");
        }
        if (!userDetails.isEnabled()) {
            return RespBean.error("账号被禁用,请联系管理员!");
        }

        // 踢掉之前的登录  
        //未添加逻辑:根据ip判断是踢掉之前的登录还是刷新token
        ValueOperations<String, Object> valueOperations= redisTemplate.opsForValue();
        String tokenStr= (String) valueOperations.get("user_"+username);
        if (!StringUtils.isBlank(tokenStr)){
            tokenStr = jwtTokenUtil.invalidateToken(tokenStr);
            valueOperations.set("user_"+username,tokenStr);
            //redis 删除key为: "user_"+username
            valueOperations.decrement("user_"+username);
        }

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        //生产token,写入redis
        String token = jwtTokenUtil.generateToken(userDetails);
        valueOperations.set("user_"+username,token);

        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put("token", token);
        tokenMap.put("tokenHead", tokenHead);
        return RespBean.success("登录成功", tokenMap);
    }

    /**
     * 根据用户名获取用户
     *
     * @param username
     * @return
     */
    @Override
    public Admin getAdminByUserName(String username) {
        return adminMapper.selectOne(new QueryWrapper<Admin>().eq("username", username));
    }

    /**
     * 根据用户id获取权限列表
     *
     * @param adminId
     * @return
     */
    @Override
    public List<Role> getRoles(Integer adminId) {
        return roleMapper.getRoles(adminId);
    }

    /**
     * 获取所有操作员
     *
     * @param Keywords
     * @return
     */
    @Override
    public List<Admin> getAllAdmins(String Keywords) {
        return adminMapper.getAllAdmins(AdminUtils.getCurrentAdmin().getId(), Keywords);
    }

    /**
     * 更新操作员角色
     *
     * @param adminId
     * @param rids
     * @return
     */
    @Override
    public RespBean updateAdminRole(Integer adminId, Integer[] rids) {
        adminRoleMapper.delete(new QueryWrapper<AdminRole>().eq("adminId", adminId));
        Integer result = adminRoleMapper.addRole(adminId, rids);
        if (rids.length == result) {
            return RespBean.success("更新成功!");
        }
        return RespBean.error("更新失败!");
    }

    /**
     * 更新用户密码
     *
     * @param oldPass
     * @param pass
     * @param adminId
     * @return
     */
    @Override
    public RespBean updatePassword(String oldPass, String pass, Integer adminId) {
        Admin admin = adminMapper.selectById(adminId);
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        if (encoder.matches(oldPass,admin.getPassword())){
            admin.setPassword(encoder.encode(pass));
            int result = adminMapper.updateById(admin);
            if (1 == result){
                return RespBean.success("更新成功!");
            }
        }
        return RespBean.error("更新失败!");
    }
}


配置SpringSecurity

package com.xx.server.config.security;

import com.xx.server.config.security.component.*;
import com.xx.server.pojo.Admin;
import com.xx.server.service.IAdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * Security配置类
 *
 * @Author xx
 * @PackageName yeb
 * @Package com.xx.server.config.security
 * @Date 2023/2/3 17:05
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private IAdminService adminService;
    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    @Autowired
    private CustomFilter customFilter;
    @Autowired
    private CustomUrlDecisionManager customUrlDecisionManager;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        //放行静态资源
        web.ignoring().antMatchers(
                "/login",
                "/fastDfs",
                "/upload",
                "/uploadStatus",
                "/logout",
                "ws",
                "/css/**",
                "/js/**",
                "/index.html",
                "/favicon.ico",
                "/doc.html",
                "/webjars/**",
                "/swagger-ui.html",
                "/swagger-resources/**",
                "/v2/api-docs/**",
                "/captcha"
        );
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //使用JWT,不需要csrf
        http.csrf()
                .disable()
                //基于Token,不需要session
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                //允许登录访问
                .antMatchers("/login", "/logout","/upload")
                .permitAll()
                //除上面外,所有请求都要求认证
                .anyRequest()
                .authenticated()
                //动态权限配置
                .withObjectPostProcessor(
                        new ObjectPostProcessor<FilterSecurityInterceptor>() {
                            @Override
                            public <O extends FilterSecurityInterceptor> O postProcess(O object){
                                object.setAccessDecisionManager(customUrlDecisionManager);
                                object.setSecurityMetadataSource(customFilter);
                                return object;
                            }
                        }
                )
                .and()
                .headers()
                .cacheControl();

        //添加jwt 登录授权过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        //添加自定义未授权和未登录结果返回
        http.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthenticationEntryPoint);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    @Bean
    public UserDetailsService userDetailsService() {
        //获取登录用户信息
        return username -> {
            Admin admin = adminService.getAdminByUserName(username);
            if (null != admin) {
                admin.setRoles(adminService.getRoles(admin.getId()));
                return admin;
            }
            throw new UsernameNotFoundException("用户名或密码不正确");
        };
    }

    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
        return new JwtAuthenticationTokenFilter();
    }
}


权限控制
CustomFilter

package com.xx.server.config.security.component;

import com.xx.server.pojo.Menu;
import com.xx.server.pojo.Role;
import com.xx.server.service.IMenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import java.util.Collection;
import java.util.List;

/**
 * 权限控制
 * @Author xx
 * @PackageName xxx
 * @Package com.xx.server.config.security.component
 * @Date 2023/2/6 15:42
 */
@Component
public class CustomFilter implements FilterInvocationSecurityMetadataSource {

    @Autowired
    private IMenuService menuService;

    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        //获取请求得url
        String requestUrl = ((FilterInvocation) o).getRequestUrl();
        //获取菜单
        List<Menu> menus = menuService.getAllMenusWithRole();
        for (Menu menu: menus) {
            //判断请求url与菜单角色是否匹配
            if (antPathMatcher.match(menu.getUrl(),requestUrl)){
                String[] str = menu.getRoles().stream().map(Role::getName).toArray(String[]::new);
                return SecurityConfig.createList(str);
            }
        }

        //没匹配得url模式为登录即可访问
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

CustomUrlDecisionManager

package com.xx.server.config.security.component;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.Collection;

/**
 * 权限控制
 * 判断用户角色
 * @Author xx
 * @PackageName xxx
 * @Package com.xx.server.config.security.component
 * @Date 2023/2/6 16:04
 */
@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        for (ConfigAttribute configAttribute : collection) {
            //当前url所需角色
            String needRole = configAttribute.getAttribute();
            //判读角色是否为登录即可访问得角色,此角色在CustomFilter中设置
            if ("ROLE_LOGIN".equals(needRole)) {
                //判读是否登录
                if (authentication instanceof AnonymousAuthenticationToken) {
                    throw new AccessDeniedException("尚未登录,请登录!");
                } else {
                    return;
                }
            }
            //判断用户角色是否为url所需角色
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足,请联系管理员!");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

添加自定义未授权及未登录的结果返回

package com.xx.server.security.component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.xx.server.pojo.RespBean;
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;
import java.io.PrintWriter;

/**
 * 当未登录或者token失效时访问接口时,自定义的返回结果
 * @Author xx
 * @PackageName xxx
 * @Package com.xx.server.config.security.component
 * @Date 2023/2/3 17:50
 */
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        PrintWriter out = response.getWriter();
        RespBean bean = RespBean.error("权限不足,请联系管理员!");
        bean.setCode(401);
        out.write(new ObjectMapper().writeValueAsString(bean));
        out.flush();
        out.close();
    }
}

package com.xx.server.security.component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.xx.server.pojo.RespBean;
import org.springframework.security.access.AccessDeniedException;
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;
import java.io.PrintWriter;

/**
 * 当访问接口没有权限时,自定义返回结果类
 * @Author xx
 * @PackageName xxx
 * @Package com.xx.server.config.security.component
 * @Date 2023/2/3 17:55
 */
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        PrintWriter out = response.getWriter();
        RespBean bean = RespBean.error("权限不足,请联系管理员!");
        bean.setCode(401);
        out.write(new ObjectMapper().writeValueAsString(bean));
        out.flush();
        out.close();
    }
}

添加jwt登录授权过滤器

package com.xx.server.security.component;

import com.xx.server.security.JWTTokenUtil;
import com.xx.server.security.JWTTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
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;

/**
 * Jwt登录授权过滤器
 * @Author xx
 * @PackageName xxx
 * @Package com.xx.server.config.security.component
 * @Date 2023/2/3 17:41
 */
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @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 {
        String authHeader = request.getHeader(this.tokenHeader);
        //存在token
        if (null!=authHeader && authHeader.startsWith(this.tokenHead)){
            String authToken  = authHeader.substring(this.tokenHead.length());
            String username=jwtTokenUtil.getUserNameFormToken(authToken);
            //token中存在用户名但未登录
            if (null!=username&&null== SecurityContextHolder.getContext().getAuthentication()){
                //登录
                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);
                }
            }
        }
        chain.doFilter(request,response);
    }
}

配置Swagger文档

添加依赖

<!-- swagger2 依赖 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.7.0</version>
        </dependency>
        <!-- Swagger第三方ui依赖 -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.9.6</version>
        </dependency>

配置Swagger

package com.xx.server.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.ArrayList;
import java.util.List;

/**
 * Swagger2配置
 *
 * @Author xx
 * @PackageName xxx
 * @Package com.xx.server.config.security
 * @Date 2023/2/6 10:35
 */
@Configuration
@EnableSwagger2
public class Swagger2Config {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                //为当前包下的controller生产api文件
                .apis(RequestHandlerSelectors.basePackage("com.yc.server.controller"))
                .paths(PathSelectors.any())
                .build()
                //添加登录认证
                .securitySchemes(securitySchemes())
                .securityContexts(securityContexts());
    }

    private List<SecurityContext> securityContexts(){
        //设置需要登录认证的路径
        List<SecurityContext> result=new ArrayList<>();
        result.add(getContextByPath("/hello/.*"));
        return result;
    }

    private SecurityContext getContextByPath(String pathRegex){
        return SecurityContext.builder()
                .securityReferences(defaultAuth())
                .forPaths(PathSelectors.regex(pathRegex))
                .build();
    }

    private List<SecurityReference> defaultAuth(){
        List<SecurityReference> result=new ArrayList<>();
        AuthorizationScope authorizationScope = new AuthorizationScope("global","accessEverything");
        AuthorizationScope[] authorizationScopes= new AuthorizationScope[1];
        authorizationScopes[0] = authorizationScope;
        result.add(new SecurityReference("Authorization",authorizationScopes));
        return result;
    }

    private ApiInfo apiInfo() {
        //设置文档信息
        return new ApiInfoBuilder()
                .title("接口文档")
                .description("接口文档")
                .contact(new Contact("xxxx","http:localhost:xxxx/doc.html","xxx@xxxx.com"))
                .version("1.0")
                .build();
    }

    private List<ApiKey> securitySchemes(){
        //设置请求头信息
        List<ApiKey> result =new ArrayList<>();
        ApiKey apikey = new ApiKey("Authorization","Authorization","header");
        result.add(apikey);
        return result;
    }


}

生成验证码

添加依赖

  <!--    &lt;!&ndash; google kaptcha依赖 &ndash;&gt;-->
        <dependency>
            <groupId>com.github.axet</groupId>
            <artifactId>kaptcha</artifactId>
            <version>0.0.9</version>
        </dependency>

验证码配置类

package com.xx.server.config;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

/**
 * 验证码配置类
 *
 * @Author xx
 * @PackageName xxx
 * @Package com.xx.server.config
 * @Date 2023/2/6 11:41
 */
@Configuration
public class CaptchaConfig {

    @Bean
    public DefaultKaptcha getDefaultKaptcha() {
        //验证码生产器
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        //配置
        Properties properties = new Properties();
        //是否有边框
        properties.setProperty("kaptcha.border", "yes");
        //设置边框颜色
        properties.setProperty("kaptcha.border.cplpr", "105,179,90");
        //边框粗细度,默认为1
        properties.setProperty("kaptcha.border.thickness", "1");
        //验证码
        properties.setProperty("kaptcha.session.key", "code");
        //验证码文本字符颜色 默认为黑色
        properties.setProperty("kaptcha.textproducer.font.color", "blue");
        //设置字体样式
        properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,,微软雅黑");
        //字体大小,默认40
        properties.setProperty("kaptcha.textproducer.font.size", "30");
        //验证码文本字符内容范围 默认为abced2345678gfynmnpwx
//        properties.setProperty("kaptcha.textproducer.char.string","");
        //字符长度,默认为5
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        //字符间距 默认为2
        properties.setProperty("kaptcha.textproducer.char.space", "4");
        //验证码图片宽度 默认为200
        properties.setProperty("kaptcha.image.width", "100");
        //验证码图片高度 默认为40
        properties.setProperty("kaptcha.image.height", "40");

        Config config = new Config(properties);
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }

}

验证码接口

package com.xx.server.controller;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;

/**
 * 验证码Controller
 *
 * @Author xx
 * @PackageName xxx
 * @Package com.xx.server.controller
 * @Date 2023/2/6 11:53
 */
@Slf4j
@RestController
public class CaptchaController {

    @Autowired
    private DefaultKaptcha defaultKaptcha;

    @ApiOperation(value = "验证码")
    @GetMapping(value = "/captcha", produces = "image/jpeg")
    public void captcha(HttpServletRequest request, HttpServletResponse response) {
        //定义response输出类型为image/jpeg类型
        response.setDateHeader("Expires", 0);
        //set standard HTTP/1.1 no-cache header
        response.setHeader("Cache-COntrol", "no-store,no-cache,must-revalidate");
        //Set IE extended HTTP/1.1 nocachee headers (use addHeader)
        response.setHeader("Cache-Control", "post-check=0,pre-check=0");
        //set standard HTTP/1.0 no-cache header
        response.setHeader("Prama", "no-cache");
        //return a jpeg
        response.setContentType("image/jpeg");

        //---------------生产验证码 begin --------------
        //获取验证码文本内容
        String text = defaultKaptcha.createText();
        log.info("验证码内容:" + text);
        //将验证码放入session中
        request.getSession().setAttribute("captcha", text);
        //根据文本内容创建图形验证码
        BufferedImage image = defaultKaptcha.createImage(text);
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            //输出流输出图片,格式jpg
            ImageIO.write(image,"jpg",outputStream);
            outputStream.flush();
        } catch (Exception e) {
          e.printStackTrace();
        } finally {
            if (null!=outputStream){
                try {
                    outputStream.close();
                } catch (Exception e) {
                  e.printStackTrace();
                }
            }
        }
        //---------------生产验证码 end --------------



    }
}

放行验证码接口

    @Override
    public void configure(WebSecurity web) throws Exception {
        //放心静态资源
        web.ignoring().antMatchers(
                "/login",
                "/fastDfs",
                "/upload",
                "/uploadStatus",
                "/logout",
                "ws",
                "/css/**",
                "/js/**",
                "/index.html",
                "/favicon.ico",
                "/doc.html",
                "/webjars/**",
                "/swagger-ui.html",
                "/swagger-resources/**",
                "/v2/api-docs/**",
                "/captcha"
        );
    }

配置redis

package com.xx.server.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * redis配置类
 * @Author xx
 * @PackageName xxx
 * @Package com.xx.server.config
 * @Date 2023/2/6 15:10
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String,Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory){
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
        //为String类型key设置序列器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //为String类型value设置序列其
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        //为hash类型key设置序列器
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        //为hash类型value设置序列器
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
 }

登录获取token

   @ApiOperation(value = "登陆之后返回token")
    @PostMapping("/login")
    public RespBean login(@RequestBody AdminLoginParam adminLoginParam, HttpServletRequest request){
        return adminService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),
                adminLoginParam.getCode(),request);
    }
    /**
     * 登录返回token
     *
     * @param username
     * @param password
     * @param request
     * @return
     */
    RespBean login(String username, String password, String code, HttpServletRequest request);


    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;
   @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private JWTTokenUtil jwtTokenUtil;

/**
     * 登录返回token
     *
     * @param username
     * @param password
     * @param code
     * @param request
     * @return
     */
    @Override
    public RespBean login(String username, String password, String code, HttpServletRequest request) {
          //1.通过用户名查询用户信息 判断用户名是否存在,账号是否被禁用
        String captcha = (String) request.getSession().getAttribute("captcha");
        if (StringUtils.isBlank(code) || !captcha.equals(code)) {
            return RespBean.error("验证码填写错误");
        }
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (null == userDetails || !passwordEncoder.matches(password, userDetails.getPassword())) {
            return RespBean.error("用户名或密码不正确!");
        }
        if (!userDetails.isEnabled()) {
            return RespBean.error("账号被禁用,请联系管理员!");
        }

        // 踢掉之前的登录
        ValueOperations<String, Object> valueOperations= redisTemplate.opsForValue();
        String tokenStr= (String) valueOperations.get("user_"+username);
        if (!StringUtils.isBlank(tokenStr)){
            // 判断当前ip和上次登录ip时候一致,一致刷新token,刷新redis
            
            tokenStr = jwtTokenUtil.invalidateToken(tokenStr);
            valueOperations.set("user_"+username,tokenStr);
            //redis 删除key为: "user_"+username
            valueOperations.decrement("user_"+username);
        }

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        //生产token
        String token = jwtTokenUtil.generateToken(userDetails);
        valueOperations.set("user_"+username,token);

        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put("token", token);
        tokenMap.put("tokenHead", tokenHead);
        return RespBean.success("登录成功", tokenMap);
    }

sql层级结构查询

循环式查询层级
package com.xx.server.pojo;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import java.util.List;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

/**
 * <p>
 * 
 * </p>
 *
 * @author xx
 * @since 2023-06-28
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_department")
@ApiModel(value="Department对象", description="")
public class Department implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "id")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @ApiModelProperty(value = "部门名称")
    private String name;

    @ApiModelProperty(value = "父id")
    private Integer parentId;

    @ApiModelProperty(value = "路径")
    private String depPath;

    @ApiModelProperty(value = "是否启用")
    private Boolean enabled;

    @ApiModelProperty(value = "是否上级")
    private Boolean isParent;

    @ApiModelProperty(value = "子部门列表")
    @TableField(exist= false)// 指定该字段不是数据库表中的字段
    private List<Department> children;

   

}

   @RequestMapping("/test")
    public String test(){

        List<Department> list = departmentMapper.getAllDepartmentsByParentId(-1);//传入需要查询的最高部门呢条数据的子部门id
        return list.toString();
    }
  /**
     * 获取所有部门
     * @param parentId
     * @return (java.lang.Integer)
     */
    List<Department> getAllDepartmentsByParentId(Integer parentId);
    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.yc.server.pojo.Department">
        <id column="id" property="id" />
        <result column="name" property="name" />
        <result column="parentId" property="parentId" />
        <result column="depPath" property="depPath" />
        <result column="enabled" property="enabled" />
        <result column="isParent" property="isParent" />
    </resultMap>

    <resultMap id="DepartmentWithChildren" type="com.yc.server.pojo.Department" extends="BaseResultMap">
        <collection property="children" ofType="com.yc.server.pojo.Department"
                    select="com.yc.server.mapper.DepartmentMapper.getAllDepartmentsByParentId"
                    column="id">
        </collection>
    </resultMap>

	  <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id, name, parentId, depPath, enabled, isParent
    </sql>


    <!-- 获取所有部门    -->
    <select id="getAllDepartmentsByParentId" resultMap="DepartmentWithChildren">
        select
        <include refid="Base_Column_List"/>
        from t_department
        where parentId = #{parentId}
    </select>


三层 树结构
    @RequestMapping("/test")
    public String test() {

//        List<Department> list = departmentMapper.getAllDepartmentsByParentId(-1);
        List<Menu> menus = menuMapper.getMenusByAdminId3(1);
        return menus.toString();
    }
package com.xx.server.pojo;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import java.util.List;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

/**
 * <p>
 * 
 * </p>
 *
 * @author xx
 * @since 2023-06-28
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_menu")
@ApiModel(value="Menu对象", description="")
public class Menu implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "id")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @ApiModelProperty(value = "url")
    private String url;

    @ApiModelProperty(value = "path")
    private String path;

    @ApiModelProperty(value = "组件")
    private String component;

    @ApiModelProperty(value = "菜单名")
    private String name;

    @ApiModelProperty(value = "图标")
    private String iconCls;

    @ApiModelProperty(value = "是否保持激活")
    private Boolean keepAlive;

    @ApiModelProperty(value = "是否要求权限")
    private Boolean requireAuth;

    @ApiModelProperty(value = "父id")
    private Integer parentId;

    @ApiModelProperty(value = "是否启用")
    private Boolean enabled;

    @ApiModelProperty(value = "子菜单")
    @TableField(exist =false)
    private List<Menu> children;

    @ApiModelProperty(value = "角色列表")
    @TableField(exist =false)
    private List<Role> roles;

}

   <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.yc.server.pojo.Menu">
        <id column="id" property="id" />
        <result column="url" property="url" />
        <result column="path" property="path" />
        <result column="component" property="component" />
        <result column="name" property="name" />
        <result column="iconCls" property="iconCls" />
        <result column="keepAlive" property="keepAlive" />
        <result column="requireAuth" property="requireAuth" />
        <result column="parentId" property="parentId" />
        <result column="enabled" property="enabled" />
    </resultMap>

    <resultMap id="Menus" type="com.yc.server.pojo.Menu" extends="BaseResultMap">
        <collection property="children" ofType="com.yc.server.pojo.Menu">
            <id column="id2" property="id"/>
            <result column="url2" property="url"/>
            <result column="path2" property="path"/>
            <result column="component2" property="component"/>
            <result column="name2" property="name"/>
            <result column="iconCls2" property="iconCls"/>
            <result column="keepAlive2" property="keepAlive"/>
            <result column="requireAuth2" property="requireAuth"/>
            <result column="parentId2" property="parentId"/>
            <result column="enabled2" property="enabled"/>
            <collection property="children" ofType="com.yc.server.pojo.Menu">
                <id column="id2" property="id"/>
                <result column="url2" property="url"/>
                <result column="path2" property="path"/>
                <result column="component2" property="component"/>
                <result column="name2" property="name"/>
                <result column="iconCls2" property="iconCls"/>
                <result column="keepAlive2" property="keepAlive"/>
                <result column="requireAuth2" property="requireAuth"/>
                <result column="parentId2" property="parentId"/>
                <result column="enabled2" property="enabled"/>

            </collection>
        </collection>
    </resultMap>



    <select id="getMenusByAdminId3" resultMap="Menuss">

        SELECT
            a.*,
            m3.id          as id3,
            m3.component   as component3,
            m3.enabled     as enabled3,
            m3.iconCls     as iconCls3,
            m3.keepAlive   as keepAlive3,
            m3.name        as name3,
            m3.parentId    as parentId3,
            m3.requireAuth as requireAuth3,
            m3.path        as path3

        FROM (

                 SELECT DISTINCT m1.*,
                                 m2.id          as id2,
                                 m2.component   as component2,
                                 m2.enabled     as enabled2,
                                 m2.iconCls     as iconCls2,
                                 m2.keepAlive   as keepAlive2,
                                 m2.name        as name2,
                                 m2.parentId    as parentId2,
                                 m2.requireAuth as requireAuth2,
                                 m2.path        as path2
                 FROM t_menu m1,
                      t_menu m2,
                      t_admin_role ar,
                      t_menu_role mr
                 WHERE m1.id = m2.parentId
                   AND ar.adminId = '1'
                   AND ar.rid = mr.rid
                   AND mr.mid = m2.id
                   AND m2.enabled = true
                 ORDER BY m1.id,
                          m2.id

             )  a
                 left JOIN t_menu m3 on  m3.parentId = a.id2

    </select>

时间类处理

注解JsonFormat

使用在类上表示:当前类的所有时间格式都是指定的格式
使用在属性上表示:当前属性为指定格式

pattern:用于指定日期类型的格式。它是一个必需属性,可以使用 SimpleDateFormat 类中定义的日期格式。例如,pattern = “yyyy-MM-dd” 表示日期格式为年份-月份-日期,以横线分隔。pattern = “yyyy-MM-dd HH:mm:ss” 表示日期格式为年份-月份-日期 时:分:秒
timezone:用于指定日期类型的时区。它是一个可选属性,可以使用时区标识符来指定时区。例如,timezone = “Asia/Shanghai” 表示使用亚洲上海的时区。
locale:用于指定地区信息。默认情况下,Jackson使用默认地区。
lenient:用于指定是否允许宽松的日期解析。默认值为false,表示不允许宽松解析。
shape:用于指定日期被序列化为何种形式,可以是字符串形式、数字形式等。常用的值有JsonFormat.Shape.STRING和JsonFormat.Shape.NUMBER。

package com.xx.server.pojo;

import java.time.LocalDateTime;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonFormat;


/**
 * <p>
 * 
 * </p>
 *
 * @author xx
 * @since 2023-06-28
 */
public class Position implements Serializable {

    private static final long serialVersionUID = 1L;

    @JsonFormat(pattern = "yyyy-MM-dd" , timezone = "Asia/Shanghai")
    private LocalDateTime createDate;



}

时间格式 LocalDate和Date

Java中的LocalDate和Date都是表示日期和时间的类,但它们之间有一些区别。

LocalDate是Java 8中引入的一个新的日期类,它表示一个不带时区的日期。LocalDate类提供了一些方法来操作日期,例如获取年份、月份和日等。它还提供了一些方法来操作日期,例如增加或减少日期、比较日期等。

Date是Java中一个比较旧的日期类,它表示一个带有时区的日期和时间。Date类的方法相对较少,它主要提供了一些方法来获取日期和时间、比较日期等。由于Date类是一个旧的类,它已经被LocalDate和其他的日期类所取代。

在使用LocalDate时,您不需要考虑时区,因为它是基于本地日期和时间进行计算的。而在使用Date时,您需要考虑时区,因为它表示的是带有时区的日期和时间。

以下是一些示例代码,用于说明如何使用LocalDate和Date:

使用LocalDate:

import java.time.LocalDate;  
  
LocalDate date = LocalDate.now(); // 获取当前日期  
int year = date.getYear(); // 获取年份  
int month = date.getMonthValue(); // 获取月份  
int day = date.getDayOfMonth(); // 获取日  
  
date = date.plusDays(7); // 增加7天

使用Date:

import java.util.Date;  
  
Date date = new Date(); // 获取当前日期和时间  
long time = date.getTime(); // 获取时间戳(毫秒)  
int year = date.getYear() + 1900; // 获取年份  
int month = date.getMonth() + 1; // 获取月份  
int day = date.getDate(); // 获取日  
  
date = new Date(time + 7 * 24 * 60 * 60 * 1000); // 增加7天(以毫秒为单位)

需要注意的是,如果您需要处理日期和时间,并且需要考虑到时区的话,建议使用Java 8中引入的新的日期和时间API,例如java.time.ZonedDateTime和java.time.ZoneId等类。这些新的API提供了更方便和更灵活的方式来处理日期和时间,同时可以更好地处理时区的问题。

springBoot的全局异常处理

RestControllerAdvice 拦截异常
ExceptionHandler自定义异常
package com.yc.server.exception;

import com.yc.server.pojo.RespBean;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.sql.SQLException;
import java.sql.SQLIntegrityConstraintViolationException;

/**
 * 全局异常
 * @Author xx
 * @PackageName yeb
 * @Package com.xx.server.exception
 * @Date 2023/2/7 14:30
 */
@RestControllerAdvice//拦截异常
public class GlobalExceptionHandler {

    @ExceptionHandler(SQLException.class)//自定义异常
    public RespBean mySQLException(SQLException e){
    	//拦截后自定义返回
        if (e instanceof SQLIntegrityConstraintViolationException){
            return RespBean.error("该数据有关数据,操作失败!");
        }
        return RespBean.error("数据库异常,操作失败!");
    }
	
	//待自定义的异常
	//.....
}

MybatisPlus分页配置

config

package com.xx.server.config;

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * MyBatis分页配置
 * @Author xx
 * @PackageName xxx
 * @Package com.xx.server.config
 * @Date 2023/2/9 13:47
 */
@Configuration
public class MyBatisPlusConfig {

    @Bean
    public PaginationInterceptor paginationInterceptor(){
        return new PaginationInterceptor();
    }
}

分页公共返回对象

package com.xx.server.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * 通过分页返回结果对象
 * @Author xx
 * @PackageName xxx
 * @Package com.xx.server.pojo
 * @Date 2023/2/9 13:49
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespPageBean {

    private Long total;
    private List<?> data;
}

全局日期格式转换

package com.xx.server.converter;

import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

/**
 * 日期转换
 * @Author xxx
 * @PackageName xxx
 * @Package com.xx.server.converter
 * @Date 2023/2/9 13:50
 */
@Component
public class DateCOnvertere implements Converter<String, LocalDate> {


    @Override
    public LocalDate convert(String s) {
        try {
            return LocalDate.parse(s, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
        } catch (Exception e) {
          e.printStackTrace();
        }
        return null;
    }
}

示例:


 /**
     * 获取所有员工(分页)
     * @param curremtPage   默认 第1页
     * @param size          默认 1页10条数据
     * @param employee
     * @param beginDateScope
     * @return
     */
    @GetMapping("/")
    public RespPageBean getEmployeeByPage(@RequestParam(defaultValue = "1")Integer curremtPage,
                                          @RequestParam(defaultValue = "10")Integer size,
                                          Employee employee, LocalDate[] beginDateScope){
        return employeeService.getEmployeeByPage(curremtPage,size,employee,beginDateScope);
    }


  /**
     * 获取所有员工(分页)
     *
     * @param curremtPage
     * @param size
     * @param employee
     * @param biginDateScope
     * @return
     */
    @Override
    public RespPageBean getEmployeeByPage(Integer curremtPage, Integer size, Employee employee, LocalDate[] biginDateScope) {
        //开启分页
        Page<Employee> page = new Page<>(curremtPage, size);
        IPage<Employee> employeeIPage = employeeMapper.getEmployeeByPage(page, employee, biginDateScope);
        RespPageBean respPageBean = new RespPageBean(employeeIPage.getTotal(), employeeIPage.getRecords());
        return respPageBean;
    }



    /**
     * 获取所有员工(分页)
     * @param page
     * @param employee
     * @param beginDateScope
     * @return
     */
    IPage<Employee> getEmployeeByPage(Page<Employee> page, @Param("employee")Employee employee,
                                      @Param("beginDateScope")LocalDate[] beginDateScope);

Easy POI 导入导出

pom
<!--    easy poi依赖-->
            <dependency>
              <groupId>cn.afterturn</groupId>
              <artifactId>easypoi-spring-boot-starter</artifactId>
              <version>4.1.3</version>
            </dependency>
使用
导出

实体类添加注解

@Excel(name = "身份证号",width = 30)
    private String idCard;

示例


    @ApiOperation(value = "导出员工数据")
    @GetMapping(value = "/export", produces = "application/octet-stream")
    public void exportEmployee(HttpServletResponse response){
        List<Employee> list =employeeService.getEmployee(null);//需要导出的实体类数据
        ExportParams params= new ExportParams("员工表","员工表", ExcelType.HSSF);//这里设置的文件名为"员工表",第一行标题为:“员工表”,类型为 HSSF,即Excel 2003及以下版本
        Workbook book= ExcelExportUtil.exportExcel(params,Employee.class,list);//生成 Workbook 对象,即Excel文件的数据结构模型。
        ServletOutputStream out = null;
        try {
            //流形式
            response.setHeader("content-type","application/octet-stream");
            //防止中文乱码
//            response.setHeader("content-disposition", "attachment;filename="+ "员工表.xls");
            response.setHeader("content-disposition", "attachment;filename="+ URLEncoder.encode("员工表.xls","UTF-8"));
//            response.setHeader("content-disposition", "attachment;filename="+ URLEncoder.encode("员工表.xls","ISO-8859-1"));
            out=response.getOutputStream();
            book.write(out);
        } catch (Exception e) {
          e.printStackTrace();
        } finally {
            if (null!=out){
                try {
                    out.flush();
                    out.close();
                } catch (Exception e) {
                  e.printStackTrace();
                }
            }
        }
    }
导入
@ApiOperation(value = "导入员工数据")
    @ApiImplicitParams({@ApiImplicitParam(name = "file",value = "上传文件",dataType="MultipartFile")})
    @PostMapping("/import")
    public RespBean importEmployee(MultipartFile file){
        ImportParams params = new ImportParams();
        //去掉标题行
        params.setTitleRows(1);
        List<Nation> nationList = nationService.list();
        List<PoliticsStatus> politicsStatusesList= politicsStatusService.list();
        List<Department> departmentList = departmentService.list();
        List<Joblevel> joblevelList = joblevelService.list();
        List<Position> positionList = positionService.list();
        try {
            List<Employee> list = ExcelImportUtil.importExcel(file.getInputStream(),Employee.class,params);
            list.forEach(employee -> {
                //民族id
                employee.setNationId(nationList.get(nationList.indexOf(new Nation(employee.getNation().getName()))).getId());
                //政治面貌id
                employee.setPoliticId(politicsStatusesList.get(politicsStatusesList.indexOf(new PoliticsStatus(employee.getPoliticsStatus().getName()))).getId());
                //部门id
                employee.setDepartmentId(departmentList.get(departmentList.indexOf(new Department().getName())).getId());
                //职称id
                employee.setJobLevelId(joblevelList.get(joblevelList.indexOf(new Joblevel(employee.getJoblevel().getName()))).getId());
                //职位id
                employee.setPosId(positionList.get(positionList.indexOf(new Position(employee.getPosition().getName()))).getId());
            });
            if (employeeService.saveBatch(list)){
                return RespBean.success("导入成功!");
            }
            return RespBean.error("导入失败!");
        } catch (Exception e) {
          e.printStackTrace();
        }
        return RespBean.error("导入失败!");
    }

邮件服务+Mq

需要根据各个邮件平台获取授权码;授权码非常重要后面需要用到
邮件服务是一个单独的服务
这里采用mq与邮件服务进行发送邮件

pom

根据具体需求添加模板

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.yc</groupId>
        <artifactId>yeb</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <groupId>com.yc</groupId>
    <artifactId>voa-mail</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>voa-mail</name>
    <!-- FIXME change it to the project's website -->
    <url>http://www.example.com</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <!--rabbitmq 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!--mail 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        <!--thymeleaf 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!--server 依赖-->
        <dependency>
            <groupId>com.yc</groupId>
            <artifactId>yeb</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.yc</groupId>
            <artifactId>yeb-server</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

    <build>
        <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
            <plugins>
                <!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
                <plugin>
                    <artifactId>maven-clean-plugin</artifactId>
                    <version>3.1.0</version>
                </plugin>
                <!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
                <plugin>
                    <artifactId>maven-resources-plugin</artifactId>
                    <version>3.0.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.0</version>
                </plugin>
                <plugin>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>2.22.1</version>
                </plugin>
                <plugin>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>3.0.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-install-plugin</artifactId>
                    <version>2.5.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-deploy-plugin</artifactId>
                    <version>2.8.2</version>
                </plugin>
                <!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
                <plugin>
                    <artifactId>maven-site-plugin</artifactId>
                    <version>3.7.1</version>
                </plugin>
                <plugin>
                    <artifactId>maven-project-info-reports-plugin</artifactId>
                    <version>3.0.0</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

yml
server:
  # 端口
  port: 8082
spring:
  # 邮件配置
  mail:
    # 邮件服务器地址 smtp.qq.com
    host: smtp.qq.com
    # 协议
    protocol: smtp
    # 编码格式
    default-encoding: utf-8
    # 授权码(在邮箱开通服务时获取
    password: xxxxx
    # 发送邮箱地址
    username: 110110@qq.com
    # 端口(不同邮箱端口号不同
    port: 587

  #rabbitmq配置
  rabbitmq:
    # 用户名
    username: xxxx
    # 密码
    password: xxxx
    # 服务器地址
    host: 127.0.0.1
    # 端口
    port: 5672
    listener:
      simple:
        # 手动确认
        acknowledge-mode: manual

  # redis配置
  redis:
    timeout: 10000ms          # 连接超时时间
    host: 127.0.0.1      # redis服务器地址
    port: 6379                # redis服务器端口
    database: 0               # 选择那个数据库,默认0库
    lettuce:
      pool:
        max-active: 1024      # 最大连接数,默认 8
        max-wait: 10000ms     # 最大连接阻塞等待时间,单位毫秒,默认-1
        max-idle: 200         # 最大空闲连接,默认8
        min-idle: 5           # 最小空闲连接,默认0

前端模板
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.theymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>欢迎邮件</title>
</head>
<body>
欢迎 <span th:text="${name}"></span>加入大家庭,
<table border="1">
    <tr>
        <td>姓名</td>
        <td th:text="${name}"></td>
    </tr>
    <tr>
        <td>职位</td>
        <td th:text="${posName}"></td>
    </tr>
    <tr>
        <td>职称</td>
        <td th:text="${joblevelName}"></td>
    </tr>
    <tr>
        <td>部门</td>
        <td th:text="${departmentName}"></td>
    </tr>
</table>

<p>
   <h1 style="color:#d20025" >你好</h1></p>
</body>

</html>

发送消息

  @Autowired
    private EmployeeMapper employeeMapper;

    @Autowired
    private RabbitTemplate rabbitTemplate;
  
  /**
     * 添加
     *
     * @param employee
     * @return
     */
    @Override
    @Transactional
    public RespBean insertEmployee(Employee employee) {
        //处理合同期限,保留两位小数
        LocalDate beginContract = employee.getBeginContract();
        LocalDate endContract = employee.getEndContract();
        long days = beginContract.until(endContract, ChronoUnit.DAYS);
        DecimalFormat decimalFormat = new DecimalFormat("##.00");
        employee.setContractTerm(Double.parseDouble(decimalFormat.format(days / 365.00)));
        if (1 == employeeMapper.insert(employee)) {

            List<Employee> ems = employeeMapper.getEmployee(employee.getId());
            Employee emp = employeeMapper.getEmployee(employee.getId()).get(0);
            //数据库记录发送的消息
            String msgId = UUID.randomUUID().toString();
            MailLog mailLog = new MailLog();
            mailLog.setMsgId(msgId);
            mailLog.setEid(emp.getId());
            mailLog.setStatus(0);
            mailLog.setRouteKey(MailConstants.MAIL_ROUTING_KEY_NAME);
            mailLog.setExchange(MailConstants.MAIl_EXCHANGE_NAEM);
            mailLog.setCount(0);
            mailLog.setTryTime(LocalDateTime.now().plusMinutes(MailConstants.MSG_TIMEOUT));
            mailLog.setCreateTime(LocalDateTime.now());
            mailLog.setUpdateTime(LocalDateTime.now());

             /**
             *  发送消息
             *  交换机  路由键   消息内容   唯一标识
             */
            rabbitTemplate.convertAndSend(MailConstants.MAIl_EXCHANGE_NAEM, MailConstants.MAIL_ROUTING_KEY_NAME, emp, new CorrelationData(msgId));
            return RespBean.success("添加成功!");
        }
        return RespBean.error("添加失败!");
    }
接收消息
package com.xx.mail.receiver;


import com.rabbitmq.client.Channel;
import com.xx.server.pojo.Employee;
import com.xx.server.pojo.MailConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Component;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

import javax.mail.internet.MimeMessage;
import java.util.Date;

/**
 * 消息接收者
 *
 * @Author xx
 * @PackageName xxx
 * @Package com.xx.mail.receiver
 * @Date 2023/2/13 14:56
 */
@Component
public class MailReceiver {

    private static final Logger logger = LoggerFactory.getLogger(MailReceiver.class);

    @Autowired
    private JavaMailSender javaMailSender;
    @Autowired
    private MailProperties mailProperties;
    @Autowired
    private TemplateEngine templateEngine;
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 邮件发送
     *
     * @param message
     * @param channel
     */
    @RabbitListener(queues = MailConstants.MAIL_QUEUE_NAME)
    public void handler(Message message, Channel channel) {
        Employee employee = (Employee) message.getPayload();
        MessageHeaders headers = message.getHeaders();
        //消息序号
        long tag = (long) headers.get(AmqpHeaders.DELIVERY_TAG);
        String msgId = (String) headers.get("spring_returned_message_correlation");
        HashOperations hashOperations = redisTemplate.opsForHash();
        try {
            if (hashOperations.entries("mail_log").containsKey(msgId)) {
                // redis中包含key,说明消息已经被消费
                logger.info("消息已经被消费======>{}",msgId);
                /**
                 * 手动确认消息
                 * tag:消息序号
                 * multiple:是否多条
                 */
                channel.basicAck(tag,false);
            }
            MimeMessage msg = javaMailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(msg);
            //发件人
            helper.setFrom(mailProperties.getUsername());
            //收件人
            helper.setTo(employee.getEmail());
            //主题
            helper.setSubject("欢迎邮件");
            //发送日期
            helper.setSentDate(new Date());
            //邮件内容
            Context context = new Context();
            context.setVariable("name", employee.getName() == null ? "xxError" : "xx");
            context.setVariable("posName", null == employee.getPosition() ? "xx" : employee.getPosition().getName());
            context.setVariable("joblevelName", employee.getJoblevel().getName() == null ? "xx" : employee.getJoblevel().getName());
            context.setVariable("departmentName", employee.getDepartment().getName() == null ? "xx" : employee.getJoblevel().getName());

            String mail = templateEngine.process("mail", context);
            helper.setText(mail, true);
            //发送邮件
            javaMailSender.send(msg);
            logger.info("邮件发送成功");
            //将消息id存入redis
            hashOperations.put("mail_log",msgId,"ok");
            //手动确认消息
            channel.basicAck(tag,false);
        } catch (Exception e) {
            try {
                /**
                 * 手动确认消息
                 * tag:消息序号
                 * multiple:是否多条
                 * requeue:是否回退到队列
                 */
                channel.basicNack(tag,false,true);
            } catch (Exception ex) {
              e.printStackTrace();
                logger.error("消息确认失败======>{}", ex.getMessage());
            }
            e.printStackTrace();
            logger.error("邮件发送失败======>{}", e.getMessage());
        }
    }

//    /**
//     * 邮件发送
//     * @param message
//     * @param channel
//     */
//    @RabbitListener(queues = MailConstants.MAIL_QUEUE_NAME)
//    public void handler(Message message , Channel channel) {
//        Employee employee = (Employee) message.getPayload();
//        MimeMessage msg = javaMailSender.createMimeMessage();
//        MimeMessageHelper helper = new MimeMessageHelper(msg);
//        try {
//            //发件人
//            helper.setFrom(mailProperties.getUsername());
//            //收件人
//            helper.setTo(employee.getEmail());
//            //主题
//            helper.setSubject("欢迎邮件");
//            //发送日期
//            helper.setSentDate(new Date());
//            //邮件内容
//            Context context = new Context();
//            context.setVariable("name", employee.getName() == null ? "xxError" : "xx");
//            context.setVariable("posName", null == employee.getPosition() ? "xx" : employee.getPosition().getName());
//            context.setVariable("joblevelName", employee.getJoblevel().getName() == null ? "xx" : employee.getJoblevel().getName());
//            context.setVariable("departmentName", employee.getDepartment().getName() == null ? "xx" : employee.getJoblevel().getName());
//
//            String mail = templateEngine.process("mail", context);
//            helper.setText(mail, true);
//            //发送邮件
//            javaMailSender.send(msg);
//        } catch (Exception e) {
//            e.printStackTrace();
//            logger.error("邮件发送失败======》{}", e.getMessage());
//        }
//    }

}

消息log实体类
package com.xx.server.pojo;

import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * <p>
 * 
 * </p>
 *
 * @author xx
 * @since 2023-02-01
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_mail_log")
@ApiModel(value="MailLog对象", description="")
public class MailLog implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "消息id")
    private String msgId;

    @ApiModelProperty(value = "接收员工id")
    private Integer eid;

    @ApiModelProperty(value = "状态(0:消息投递中 1:投递成功 2:投递失败)")
    private Integer status;

    @ApiModelProperty(value = "路由键")
    private String routeKey;

    @ApiModelProperty(value = "交换机")
    private String exchange;

    @ApiModelProperty(value = "重试次数")
    private Integer count;

    @ApiModelProperty(value = "重试时间")
    private LocalDateTime tryTime;

    @ApiModelProperty(value = "创建时间")
    private LocalDateTime createTime;

    @ApiModelProperty(value = "更新时间")
    private LocalDateTime updateTime;


}

消息状态常量
package com.xx.server.pojo;

/**
 * 消息状态
 *
 * @Author xx
 * @PackageName xxx
 * @Package com.xx.server.pojo
 * @Date 2023/2/14 11:31
 */
public class MailConstants {

    //消息投递中
    public static final Integer DELIVERING = 0;
    //消息投递成功
    public static final Integer SUCCESS = 1;
    //消息投递失败
    public static final Integer FAILURE = 2;
    //最大重试次数
    public static final Integer MAC_TRY_COUNT = 3;
    //消息超时时间
    public static final Integer MSG_TIMEOUT = 1;
    //队列
    public static final String MAIL_QUEUE_NAME = "mail.queue";
    //交换机
    public static final String MAIl_EXCHANGE_NAEM = "mail_exchange";
    //路由键
    public static final String MAIL_ROUTING_KEY_NAME = "mail.routing.key";

}

配置mq 开启消息确认回调以及消息失败回调
package com.xx.server.config;

import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.yc.server.pojo.MailConstants;
import com.xx.server.pojo.MailLog;
import com.yc.server.service.IMailLogService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


/**
 * RabbitMQ配置类
 *
 * @Author xx
 * @PackageName xxx
 * @Package com.xx.server.config
 * @Date 2023/2/14 13:56
 */
@Configuration
public class RabbitMqConfig {

    public static final Logger Logger = LoggerFactory.getLogger(RabbitMqConfig.class);
    @Autowired
    private CachingConnectionFactory cachingConnectionFactory;
    @Autowired
    private IMailLogService mailLogService;

    @Bean
    public RabbitTemplate rabbitTemplate() {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory);
        /**
         * 消息确认回调,确认消息是否到达broker
         * data:消息唯一标识
         * ack: 确认结果
         * cause:失败原因
         */
        rabbitTemplate.setConfirmCallback((data, ack, cause) -> {
            String msgId = data.getId();
            if (ack) {
                //消息确认成功
                Logger.info("{}====>消息发送成功", msgId);
                //更新数据库中记录
                mailLogService.update(new UpdateWrapper<MailLog>().set("status", 1).eq("msgId", msgId));
            } else {
                Logger.info("{}====>消息发送失败", msgId);
            }
        });
        /**
         * 消息失败回调,比如router不到queue时回调
         * msg:消息主题
         * repCode:响应码
         * repText:响应描述
         * exchange:交换机
         * routingKey:路由键
         *
         */
        rabbitTemplate.setReturnCallback((msg, repCode, reText, exchange, routingKey) -> {
            Logger.info("{}======>消息发送到queue时失败", msg.getBody());
        });
        return rabbitTemplate;
    }

    @Bean
    public Queue queue() {
        return new Queue(MailConstants.MAIL_QUEUE_NAME, true);
    }

    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange(MailConstants.MAIl_EXCHANGE_NAEM);
    }

    @Bean
    public Binding binding() {
        return BindingBuilder.bind(queue()).to(directExchange()).with(MailConstants.MAIL_ROUTING_KEY_NAME);
    }
}

mq回调yml配置

spring:
  # rabbitmq配置
  rabbitmq:
    # 用户名
    username: xxxx
    # 密码
    password: xxxx
    # 服务器地址
    host: 127.0.0.1
    # 端口
    port: 5672
    # 消息失败回调
    publisher-returns: true
    # 消息确认回调
    publisher-confirm-type: correlated
定时任务重新发送失败消息

缺点:一致开启定时任务会比较消耗资源

package com.xx.server.task;

import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.xx.server.pojo.Employee;
import com.xx.server.pojo.MailConstants;
import com.xx.server.pojo.MailLog;
import com.xx.server.service.IEmployeeService;
import com.xx.server.service.IMailLogService;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.*;

/**
 * 右键发送定时任务
 *
 * @Author xx
 * @PackageName xxx
 * @Package com.xx.server.task
 * @Date 2023/2/14 14:16
 */
@Component
public class MailTask {
    @Autowired
    private IMailLogService mailLogService;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private IEmployeeService employeeService;

    /**
     * 邮件发送定时任务
     * 10秒一次
     */
    @Scheduled(cron = "0/10 * * * * ?")
    public void mailTask() {
        //状态为0且重试时间小于当前时间的才需要重新发送
        List<MailLog> list = mailLogService.list(new UpdateWrapper<MailLog>().eq("status", 0).lt("tryTime", LocalDateTime.now()));
        list.forEach(mailLog -> {
            //重试次数超过3次,更新为投递失败,不再重试
            if (3 <= mailLog.getCount()) {
                mailLogService.update(new UpdateWrapper<MailLog>().set("status", 2).eq("msgId", mailLog.getMsgId()));
            }
            //更新重试次数,更新时间,重试时间
            mailLogService.update(new UpdateWrapper<MailLog>().set("count", mailLog.getCount() + 1).set("updateTime", LocalDateTime.now()).set("tryTime", LocalDateTime.now().plusMinutes(MailConstants.MSG_TIMEOUT)).eq("msgId", mailLog.getMsgId()));
            Employee emp = employeeService.getEmployee(mailLog.getEid()).get(0);
            //发送消息
            rabbitTemplate.convertAndSend(MailConstants.MAIl_EXCHANGE_NAEM, MailConstants.MAIL_ROUTING_KEY_NAME, emp, new CorrelationData(mailLog.getMsgId()));
        });

    }
}

异步调用

SpringBoot启动类上添加 @EnableAsync注解,开启基于注解的异步任务支持
在方法上添加@Async注解将方法aaa()标注为异步方法

无返回值异步调用
有返回值异步调用
总结

无返回值异步调用不会出现阻塞,继续向下执行主流程程序,调用的异步方法会作为一个子线程单独执行,直到异步方法执行完成
有返回值异步待用会出现阻塞,当返回值较多时主流程在执行异步方法是会有短暂阻塞,需要等待并获取异步方法的返回结果,而调用的多个异步方法会作为两个子线程并行执行,直到异步方法执行完成并返回结果,主流线程会在最后一个异步方法返回结果后跳出阻塞状态。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值