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>
<!-- <!–security 依赖–>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- <!–JWT 依赖–>-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!-- <!– google kaptcha依赖 –>-->
<dependency>
<groupId>com.github.axet</groupId>
<artifactId>kaptcha</artifactId>
<version>0.0.9</version>
</dependency>
<!-- <!– spring data redis 依赖 –>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- <!– commons-pool2 对象池依赖 –>-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- <!–easy poi依赖–>-->
<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>-->
<!-- <!–web 依赖–>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-web</artifactId>-->
<!-- </dependency>-->
<!-- <!–lombok 依赖–>-->
<!-- <dependency>-->
<!-- <groupId>org.projectlombok</groupId>-->
<!-- <artifactId>lombok</artifactId>-->
<!-- </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>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-autoconfigure</artifactId>-->
<!-- <version>2.7.5</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>–>-->
<!-- <!– </dependency>–>-->
<!--<!– <dependency>–>-->
<!--<!– <groupId>com.github.xiaoymin</groupId>–>-->
<!--<!– <artifactId>knife4j-spring-ui</artifactId>–>-->
<!--<!– </dependency>–>-->
<!-- </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
# 空闲连接存活最大时间,默认600000(10分钟)
idle-timeout: 180000
# 最大连接数,默认10
maximum-pool-size: 10
# 从连接池返回的连接的自动提交
auto-commit: true
# 连接最大存活时间,0表示永久存活,默认1800000(30分钟)
max-lifetime: 1800000
# 连接超时时间,默认30000(30秒)
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;
}
}
生成验证码
添加依赖
<!-- <!– google kaptcha依赖 –>-->
<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()标注为异步方法
无返回值异步调用
有返回值异步调用
总结
无返回值异步调用不会出现阻塞,继续向下执行主流程程序,调用的异步方法会作为一个子线程单独执行,直到异步方法执行完成
有返回值异步待用会出现阻塞,当返回值较多时主流程在执行异步方法是会有短暂阻塞,需要等待并获取异步方法的返回结果,而调用的多个异步方法会作为两个子线程并行执行,直到异步方法执行完成并返回结果,主流线程会在最后一个异步方法返回结果后跳出阻塞状态。