完成管理后台用户管理及登陆

This commit is contained in:
xpecya 2024-12-05 13:08:22 +08:00
commit 7f06178d2f
25 changed files with 1756 additions and 0 deletions

35
.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/*
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

49
admin/pom.xml Normal file
View File

@ -0,0 +1,49 @@
<?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.jinrui</groupId>
<artifactId>reference</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>admin</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.jinrui</groupId>
<artifactId>core</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,36 @@
package com.jinrui.reference.admin;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.util.StringUtils;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
@SpringBootApplication(scanBasePackages = {
"com.jinrui.reference.admin",
"com.jinrui.reference.core"
})
@MapperScan({
"com.jinrui.reference.admin.mapper",
"com.jinrui.reference.core.mapper"})
public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}
@Bean
public JedisPool jedisPool(@Value("${redis.host}") String host,
@Value("${redis.port}") int port,
@Value("${redis.timeout:1000}") int timeout,
@Value("${redis.password:}") String password) {
if (StringUtils.hasText(password)) {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
return new JedisPool(jedisPoolConfig, host, port, timeout, password);
}
return new JedisPool(host, port);
}
}

View File

@ -0,0 +1,215 @@
package com.jinrui.reference.admin.controller;
import com.jinrui.reference.admin.model.dto.login.AdminUserBanDTO;
import com.jinrui.reference.admin.model.dto.login.AdminUserCreateDTO;
import com.jinrui.reference.admin.model.dto.login.AdminUserDeleteDTO;
import com.jinrui.reference.admin.model.entity.AdminUser;
import com.jinrui.reference.admin.model.vo.admin.user.AdminUserVO;
import com.jinrui.reference.admin.service.AdminJwtService;
import com.jinrui.reference.admin.service.AdminUserService;
import com.jinrui.reference.core.model.vo.PageObject;
import com.jinrui.reference.core.model.vo.ResultObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.DeleteMapping;
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.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
@RestController
@RequestMapping("/admin/user")
public class AdminUserController {
private static final Logger log = LoggerFactory.getLogger(AdminUserController.class);
private final AdminUserService adminUserService;
public AdminUserController(AdminUserService adminUserService) {
this.adminUserService = adminUserService;
}
/**
* 管理后台分页搜索接口
*
* @param token 登陆Token
* @param name 用户昵称 模糊搜索
* @param phone 用户手机号 模糊搜索
* @param page 分页参数 当前页码 默认第一页
* @param size 分页参数 当前页长度 默认10条
* @return 分页搜索结果
*/
@GetMapping
public PageObject<AdminUserVO> queryAdminUser(@RequestHeader("auth-token") String token,
@RequestParam(value = "name", required = false) String name,
@RequestParam(value = "phone", required = false) String phone,
@RequestParam(value = "page", required = false, defaultValue = "1") int page,
@RequestParam(value = "size", required = false, defaultValue = "10") int size) {
if (!StringUtils.hasText(token)) {
return PageObject.failedPage("登陆Token为空!");
}
try {
AdminUser adminUser = AdminJwtService.parseToken(token);
if (adminUser == null) {
log.warn("解析token {}拿不到AdminUser对象!", token);
return PageObject.failedPage("登陆Token有误请联系系统管理员!");
}
if (!adminUser.isActive()) {
log.warn("当前用户已被封禁! id = {}", adminUser.getId());
return PageObject.failedPage("当前用户已被封禁!请联系系统管理员!");
}
log.info("path: /admin/user, method: GET, request user id: {}, name: {}, phone: {}, page: {}, size: {}",
adminUser.getId(), name, phone, page, size);
} catch (Exception e) {
log.error("解析登陆Token出错!", e);
return PageObject.failedPage(500, "服务端错误,请联系系统管理员!");
}
return adminUserService.findAdminUser(name, phone, page, size);
}
/**
* 管理后台用户封禁接口
*
* @param token 登陆Token
* @param adminUserBanDTO 要封禁的用户ID
* @return 封禁结果
*/
@PostMapping("/ban")
public ResultObject<Void> ban(@RequestHeader("auth-token") String token,
@RequestBody AdminUserBanDTO adminUserBanDTO) {
if (!StringUtils.hasText(token)) {
return ResultObject.failed("登陆Token为空!");
}
Long id = adminUserBanDTO.getId();
if (id == null) {
log.warn("要封禁的用户ID为空!");
return ResultObject.failed("要封禁的用户ID为空!");
}
try {
AdminUser adminUser = AdminJwtService.parseToken(token);
if (adminUser == null) {
log.warn("解析token {}拿不到AdminUser对象!", token);
return ResultObject.failed("登陆Token有误请联系系统管理员!");
}
if (!adminUser.isActive()) {
log.warn("当前用户已被封禁! id = {}", adminUser.getId());
return ResultObject.failed("当前用户已被封禁!请联系系统管理员!");
}
Long adminUserId = adminUser.getId();
if (Objects.equals(adminUserId, id)) {
log.warn("用户(id = {})尝试自己封禁自己,已禁止该操作", id);
return ResultObject.failed("请勿自己封禁自己!");
}
log.info("path: /admin/user/ban, method: POST, request user id: {}, ban user id: {}", adminUserId, id);
} catch (Exception e) {
log.error("解析登陆Token出错!", e);
return ResultObject.failed(500, "服务端错误,请联系系统管理员!");
}
return adminUserService.ban(id);
}
/**
* 管理后台删除用户接口
*
* @param token 登陆Token
* @param adminUserDeleteDTO 要删除的用户ID
* @return 删除结果
*/
@DeleteMapping
public ResultObject<Void> delete(@RequestHeader("auth-token") String token,
@RequestBody AdminUserDeleteDTO adminUserDeleteDTO) {
if (!StringUtils.hasText(token)) {
return ResultObject.failed("登陆Token为空!");
}
Long id = adminUserDeleteDTO.getId();
if (id == null) {
log.warn("要删除的用户ID为空!");
return ResultObject.failed("要删除的用户ID为空!");
}
try {
AdminUser adminUser = AdminJwtService.parseToken(token);
if (adminUser == null) {
log.warn("解析token {}拿不到AdminUser对象!", token);
return ResultObject.failed("登陆Token有误请联系系统管理员!");
}
if (!adminUser.isActive()) {
log.warn("当前用户已被封禁! id = {}", adminUser.getId());
return ResultObject.failed("当前用户已被封禁!请联系系统管理员!");
}
Long adminUserId = adminUser.getId();
if (Objects.equals(adminUserId, id)) {
log.warn("用户(id = {})尝试自己删除自己,已禁止该操作", id);
return ResultObject.failed("请勿自己删除自己!");
}
log.info("path: /admin/user, method: DELETE, request user id: {}, delete user id: {}", adminUserId, id);
} catch (Exception e) {
log.error("解析登陆Token出错!", e);
return ResultObject.failed(500, "服务端错误,请联系系统管理员!");
}
return adminUserService.delete(id);
}
/**
* 管理后台创建用户接口
*
* @param token 登陆Token
* @param adminUserCreateDTO 要创建的用户昵称及手机号
* @return 创建结果
*/
@PostMapping("/create")
public ResultObject<Void> createUser(@RequestHeader("auth-token") String token,
@RequestBody AdminUserCreateDTO adminUserCreateDTO) {
if (!StringUtils.hasText(token)) {
return ResultObject.failed("登陆Token为空!");
}
String phone = adminUserCreateDTO.getPhone();
if (!StringUtils.hasText(phone)) {
return ResultObject.failed("手机号不可为空!");
}
String name = adminUserCreateDTO.getName();
try {
AdminUser adminUser = AdminJwtService.parseToken(token);
if (adminUser == null) {
log.warn("解析token {}拿不到AdminUser对象!", token);
return ResultObject.failed("登陆Token有误请联系系统管理员!");
}
if (!adminUser.isActive()) {
log.warn("当前用户已被封禁! id = {}", adminUser.getId());
return ResultObject.failed("当前用户已被封禁!请联系系统管理员!");
}
log.info("path: /admin/user/create, method: POST, request user id: {}, name: {}, phone: {}",
adminUser.getId(), name, phone);
} catch (Exception e) {
log.error("解析登陆Token出错!", e);
return ResultObject.failed(500, "服务端错误,请联系系统管理员!");
}
return adminUserService.create(name, phone);
}
}

View File

@ -0,0 +1,47 @@
package com.jinrui.reference.admin.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jinrui.reference.admin.model.dto.login.LoginDTO;
import com.jinrui.reference.admin.model.dto.login.SendCaptchaDTO;
import com.jinrui.reference.admin.model.vo.login.LoginVO;
import com.jinrui.reference.admin.service.AdminUserService;
import com.jinrui.reference.core.controller.BaseController;
import com.jinrui.reference.core.model.vo.ResultObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/login")
public class LoginController extends BaseController {
private static final Logger log = LoggerFactory.getLogger(LoginController.class);
private final AdminUserService adminUserService;
public LoginController(ObjectMapper objectMapper,
AdminUserService adminUserService) {
super(objectMapper);
this.adminUserService = adminUserService;
}
@PostMapping("/captcha")
public ResultObject<String> sendCaptcha(@RequestBody SendCaptchaDTO sendCaptchaDTO) {
String phone = sendCaptchaDTO.getPhone();
log.info("path: /login/captcha, phone: {}", phone);
ResultObject<String> resultObject = adminUserService.sendCaptcha(phone);
super.logResponse(resultObject);
return resultObject;
}
@PostMapping
public ResultObject<LoginVO> login(@RequestBody LoginDTO loginDTO) {
log.info("path: /login, phone: {}, captcha: {}", loginDTO.getPhone(), loginDTO.getCaptcha());
ResultObject<LoginVO> resultObject = adminUserService.login(loginDTO);
super.logResponse(resultObject);
return resultObject;
}
}

View File

@ -0,0 +1,68 @@
package com.jinrui.reference.admin.mapper;
import com.jinrui.reference.admin.model.entity.AdminUser;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.apache.ibatis.type.JdbcType;
import java.util.Date;
import java.util.List;
public interface AdminUserMapper {
@Results({
@Result(column = "id", property = "id", id = true),
@Result(column = "phone", property = "phone"),
@Result(column = "name", property = "name"),
@Result(column = "active", property = "active"),
@Result(column = "create_time", property = "createTime", javaType = Date.class, jdbcType = JdbcType.TIMESTAMP),
@Result(column = "update_time", property = "updateTime", javaType = Date.class, jdbcType = JdbcType.TIMESTAMP)
})
@Select("select * from admin_user where phone = #{phone}")
AdminUser getAdminUserByPhone(@Param("phone") String phone);
@Results({
@Result(column = "id", property = "id", id = true),
@Result(column = "phone", property = "phone"),
@Result(column = "name", property = "name"),
@Result(column = "active", property = "active"),
@Result(column = "create_time", property = "createTime", javaType = Date.class, jdbcType = JdbcType.TIMESTAMP),
@Result(column = "update_time", property = "updateTime", javaType = Date.class, jdbcType = JdbcType.TIMESTAMP)
})
@Select("<script>" +
"select * from admin_user " +
"<where>" +
"<if test=\"name != null and !name.isEmpty()\">" +
"name like concat('%', #{name}, '%') " +
"</if>" +
"<if test=\"phone != null and !phone.isEmpty()\">" +
"and phone like concat('%', #{phone}, '%') " +
"</if>" +
"</where>" +
"</script>")
List<AdminUser> queryAdminUser(@Param("name") String name, @Param("phone") String phone);
@Update("update admin_user set active = 0, update_time = now() where id = #{id}")
void ban(@Param("id") long id);
@Delete("delete from admin_user where id = #{id}")
void delete(@Param("id") long id);
@Insert("<script>" +
"insert into admin_user(phone, create_time, update_time" +
"<if test=\"name != null and !name.isEmpty()\">" +
", name" +
"</if>" +
") values (#{phone}, now(), now()" +
"<if test=\"name != null and !name.isEmpty()\">" +
", #{name}" +
"</if>" +
")" +
"</script>")
void create(@Param("name") String name, @Param("phone") String phone);
}

View File

@ -0,0 +1,20 @@
package com.jinrui.reference.admin.model.dto.login;
/**
* 管理后台禁用用户DTO
*/
public class AdminUserBanDTO {
/**
* 被禁用的用户ID
*/
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}

View File

@ -0,0 +1,33 @@
package com.jinrui.reference.admin.model.dto.login;
/**
* 管理后台创建用户DTO
*/
public class AdminUserCreateDTO {
/**
* 用户昵称
*/
private String name;
/**
* 用户手机号 不可为空
*/
private String phone;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}

View File

@ -0,0 +1,20 @@
package com.jinrui.reference.admin.model.dto.login;
/**
* 管理后台删除用户DTO
*/
public class AdminUserDeleteDTO {
/**
* 被删除的用户ID
*/
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}

View File

@ -0,0 +1,25 @@
package com.jinrui.reference.admin.model.dto.login;
@SuppressWarnings("unused")
public class LoginDTO {
private String phone;
private String captcha;
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getCaptcha() {
return captcha;
}
public void setCaptcha(String captcha) {
this.captcha = captcha;
}
}

View File

@ -0,0 +1,15 @@
package com.jinrui.reference.admin.model.dto.login;
@SuppressWarnings("unused")
public class SendCaptchaDTO {
private String phone;
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}

View File

@ -0,0 +1,125 @@
package com.jinrui.reference.admin.model.entity;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Date;
import java.util.Map;
/**
* 管理后台用户
*/
@SuppressWarnings("unused")
public class AdminUser {
/**
* 用户ID
*/
private Long id;
/**
* 用户手机号
*/
private String phone;
/**
* 用户名称
*/
private String name;
/**
* <p>当前用户状态 是否启用</p>
* true - 启动 | false - 禁用
*/
private boolean active;
/**
* 用户创建时间
*/
private Date createTime;
/**
* 用户修改时间
*/
private Date updateTime;
public AdminUser() {}
public AdminUser(DecodedJWT decodedJWT) {
for (Map.Entry<String, Claim> entry : decodedJWT.getClaims().entrySet()) {
String keyName = entry.getKey();
Claim value = entry.getValue();
switch (keyName) {
case "id": {
this.id = value.asLong();
break;
}
case "phone": {
this.phone = value.asString();
break;
}
case "name": {
this.name = value.asString();
break;
}
case "createTime": {
this.createTime = new Date(value.asLong());
break;
}
case "updateTime": {
this.updateTime = new Date(value.asLong());
break;
}
}
}
this.active = true;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
}

View File

@ -0,0 +1,103 @@
package com.jinrui.reference.admin.model.vo.admin.user;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.jinrui.reference.admin.model.entity.AdminUser;
import java.util.Date;
/**
* 管理后台用户
*/
@SuppressWarnings("unused")
public class AdminUserVO {
/**
* 用户ID
*/
private Long id;
/**
* 用户手机号
*/
private String phone;
/**
* 用户名称
*/
private String name;
/**
* <p>当前用户状态 是否启用</p>
* true - 启动 | false - 禁用
*/
private boolean active;
/**
* 用户创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 用户修改时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
public AdminUserVO(AdminUser adminUser) {
this.id = adminUser.getId();
this.phone = adminUser.getPhone();
this.name = adminUser.getName();
this.active = adminUser.isActive();
this.createTime = adminUser.getCreateTime();
this.updateTime = adminUser.getUpdateTime();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
}

View File

@ -0,0 +1,103 @@
package com.jinrui.reference.admin.model.vo.login;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.jinrui.reference.admin.model.entity.AdminUser;
import com.jinrui.reference.admin.service.AdminJwtService;
import java.util.Date;
/**
* 登陆返回值
*/
@SuppressWarnings("unused")
public class LoginVO {
/**
* 用户ID
*/
private Long id;
/**
* 用户手机号
*/
private String phone;
/**
* 用户昵称
*/
private String name;
/**
* 用户登陆Token
*/
private String token;
/**
* 用户创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 用户修改时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
public LoginVO(AdminUser adminUser) {
this.id = adminUser.getId();
this.phone = adminUser.getPhone();
this.name = adminUser.getName();
this.token = AdminJwtService.generateToken(adminUser);
this.createTime = adminUser.getCreateTime();
this.updateTime = adminUser.getUpdateTime();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
}

View File

@ -0,0 +1,62 @@
package com.jinrui.reference.admin.service;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.jinrui.reference.admin.model.entity.AdminUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class AdminJwtService {
private static final Logger log = LoggerFactory.getLogger(AdminJwtService.class);
private static final String SECRET = "R!E@F#E$R%E^N&C*E";
private AdminJwtService() {}
/**
* 管理后台根据用户对象生成JWT TOKEN
*
* @param adminUser 管理后台用户对象
* @return JWT TOKEN
*/
public static String generateToken(AdminUser adminUser) {
Long id = adminUser.getId();
String phone = adminUser.getPhone();
String name = adminUser.getName();
long createTime = adminUser.getCreateTime().getTime();
long updateTime = adminUser.getUpdateTime().getTime();
JWTCreator.Builder jwtBuilder = JWT.create();
jwtBuilder.withClaim("id", id);
jwtBuilder.withClaim("phone", phone);
jwtBuilder.withClaim("name", name);
jwtBuilder.withClaim("createTime", createTime);
jwtBuilder.withClaim("updateTime", updateTime);
jwtBuilder.withClaim("timestamp", System.currentTimeMillis());
return jwtBuilder.sign(Algorithm.HMAC256(SECRET));
}
public static AdminUser parseToken(String token) {
if (token == null) {
throw new NullPointerException("token is null!");
}
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET))
.withClaimPresence("id")
.withClaimPresence("phone")
.withClaimPresence("name")
.withClaimPresence("createTime")
.withClaimPresence("updateTime")
.withClaimPresence("timestamp")
.build();
try {
DecodedJWT decodedJWT = jwtVerifier.verify(token);
return new AdminUser(decodedJWT);
} catch (JWTVerificationException e) {
log.error("error in verifying token!", e);
}
return null;
}
}

View File

@ -0,0 +1,288 @@
package com.jinrui.reference.admin.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jinrui.reference.admin.mapper.AdminUserMapper;
import com.jinrui.reference.admin.model.dto.login.LoginDTO;
import com.jinrui.reference.admin.model.entity.AdminUser;
import com.jinrui.reference.admin.model.vo.admin.user.AdminUserVO;
import com.jinrui.reference.admin.model.vo.login.LoginVO;
import com.jinrui.reference.core.model.vo.PageObject;
import com.jinrui.reference.core.model.vo.ResultObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.stream.Collectors;
@Service
public class AdminUserService {
private static final String REDIS_LOGIN_LOCK_KEY = "login-lock-";
private static final String REDIS_KEY_SEND_CAPTCHA_TO_PREFIX = "send-captcha-to-";
private static final String REDIS_KEY_CAPTCHA = "login-captcha-";
private static final Logger log = LoggerFactory.getLogger(AdminUserService.class);
private final JedisPool jedisPool;
private final AdminUserMapper adminUserMapper;
private final ObjectMapper objectMapper;
public AdminUserService(JedisPool jedisPool,
AdminUserMapper adminUserMapper,
ObjectMapper objectMapper) {
this.jedisPool = jedisPool;
this.adminUserMapper = adminUserMapper;
this.objectMapper = objectMapper;
}
/**
* <p>给指定手机号发送验证码如果不是系统中已有的手机号则不发送被禁用的账号不发送</p>
* <p>同一个手机号60秒之内只能发送一封验证码短信验证码有效期10分钟</p>
*
* @param phone 要发送验证码的手机号
*/
public ResultObject<String> sendCaptcha(String phone) {
if (!StringUtils.hasText(phone)) {
return ResultObject.failed("手机号为空!");
}
String redisLockKey = getLoginLockKey(phone);
try (Jedis jedis = jedisPool.getResource()) {
long incrResult = jedis.incr(redisLockKey);
if (incrResult > 1) {
jedis.decr(redisLockKey);
return ResultObject.failed("系统繁忙,请稍后再试!");
}
jedis.expire(redisLockKey, 3L);
String redisCaptchaKey = getRedisCaptchaKey(phone);
incrResult = jedis.incr(redisCaptchaKey);
if (incrResult > 1) {
log.warn("手机号{}在一分钟内已发送过短信,请勿重复发送!", phone);
jedis.decr(redisLockKey);
jedis.decr(redisCaptchaKey);
return ResultObject.failed("该手机号一分钟内已发送过短信,请勿重复发送!");
}
jedis.expire(redisCaptchaKey, 60L);
AdminUser adminUser = adminUserMapper.getAdminUserByPhone(phone);
if (adminUser == null) {
String errorMessage = String.format("找不到手机号为%s的用户!", phone);
log.warn(errorMessage);
jedis.decr(redisLockKey);
return ResultObject.failed(errorMessage);
}
if (!adminUser.isActive()) {
log.warn("被禁用的用户(phone = {})尝试登陆!", phone);
jedis.decr(redisLockKey);
return ResultObject.failed("当前用户已被禁用,请联系系统管理员!");
}
String randomCaptcha = randomCaptcha();
log.info("记录手机号{}的登陆验证码: {}", phone, randomCaptcha);
String redisCaptchaListKey = getRedisCaptchaListKey(phone);
String captchaList = jedis.get(redisCaptchaListKey);
if (!StringUtils.hasText(captchaList) || "nil".equals(captchaList)) {
captchaList = randomCaptcha;
} else {
captchaList += ("," + randomCaptcha);
}
log.info("手机号{}当前的验证码列表: {}", phone, captchaList);
jedis.set(redisCaptchaListKey, captchaList);
jedis.expire(redisCaptchaListKey, 600L);
jedis.decr(redisLockKey);
return ResultObject.success(randomCaptcha);
} catch (Exception e) {
log.error("sendCaptcha异常!", e);
return ResultObject.failed(500, "服务端错误,请联系系统管理员!");
}
}
/**
* 根据验证码进行登陆登陆后给当前手机发送的所有验证码无效
*
* @param loginDTO 登陆参数
* @return 登陆结果
*/
public ResultObject<LoginVO> login(LoginDTO loginDTO) {
String phone = loginDTO.getPhone();
if (!StringUtils.hasText(phone)) {
return ResultObject.failed("手机号为空!");
}
String captcha = loginDTO.getCaptcha();
if (!StringUtils.hasText(captcha)) {
return ResultObject.failed("验证码为空!");
}
int captchaLength = captcha.length();
if (captchaLength != 6) {
log.warn("手机号{}登陆时试图使用错误的验证码{}", phone, captcha);
return ResultObject.failed("验证码错误,请重新输入!");
}
String redisLockKey = getLoginLockKey(phone);
try (Jedis jedis = jedisPool.getResource()) {
long incrResult = jedis.incr(redisLockKey);
if (incrResult > 1) {
jedis.decr(redisLockKey);
return ResultObject.failed("系统繁忙,请稍后再试!");
}
jedis.expire(redisLockKey, 3L);
String redisCaptchaListKey = getRedisCaptchaListKey(phone);
String captchaList = jedis.get(redisCaptchaListKey);
if (!StringUtils.hasText(captchaList)) {
log.warn("没有找到给手机号{}发送的验证码!", phone);
jedis.decr(redisLockKey);
return ResultObject.failed("验证码错误,请重新输入!");
}
String[] split = captchaList.split(",");
for (String available : split) {
if (Objects.equals(captcha, available)) {
log.info("手机号{}验证码校验成功!", phone);
AdminUser adminUser = adminUserMapper.getAdminUserByPhone(phone);
if (adminUser == null) {
return ResultObject.failed("用户不存在,请联系系统管理员!");
}
if (!adminUser.isActive()) {
return ResultObject.failed("用户已被禁用,请联系系统管理员!");
}
LoginVO loginVO = new LoginVO(adminUser);
jedis.del(redisLockKey, redisCaptchaListKey);
return ResultObject.success(loginVO);
}
}
log.info("没有给手机{}发送过验证码{}, 已发送过的验证码是[{}]", phone, captcha, captchaList);
jedis.decr(redisLockKey);
return ResultObject.failed("验证码错误,请重新输入!");
} catch (Exception e) {
log.error("login错误!", e);
return ResultObject.failed(500, "服务端错误,请联系系统管理员!");
}
}
/**
* 查询管理后台用户
*
* @param name 用户名 模糊匹配
* @param phone 手机号 模糊匹配
* @param page 当前页码 默认第一页
* @param size 每页长度 默认10条
* @return 查询结果
*/
public PageObject<AdminUserVO> findAdminUser(String name, String phone, int page, int size) {
List<AdminUser> resultList = adminUserMapper.queryAdminUser(name, phone);
if (CollectionUtils.isEmpty(resultList)) {
return PageObject.empty(page);
}
PageObject<AdminUserVO> pageObject = new PageObject<>();
pageObject.setTotal(resultList.size());
List<AdminUserVO> responseList = resultList.stream()
.skip((long) (page - 1) * size)
.limit(size)
.map(AdminUserVO::new)
.collect(Collectors.toList());
pageObject.setCurrent(page);
pageObject.setSize(Math.min(size, resultList.size()));
pageObject.setCode(200);
pageObject.setData(responseList);
try {
String pageString = objectMapper.writeValueAsString(pageObject);
log.info("查询管理后台结果: {}", pageString);
} catch (JsonProcessingException e) {
log.error("查询用户返回分页对象json映射报错!", e);
}
return pageObject;
}
/**
* 管理后台封禁用户接口
*
* @param id 要封禁的用户ID
* @return 封禁结果
*/
public ResultObject<Void> ban(Long id) {
try {
adminUserMapper.ban(id);
} catch (Exception e) {
log.error("ban错误!", e);
return ResultObject.failed(500, "服务端错误!请联系系统管理员!");
}
return ResultObject.success();
}
/**
* 管理后台删除用户接口
*
* @param id 要删除的用户ID
* @return 删除结果
*/
public ResultObject<Void> delete(Long id) {
try {
adminUserMapper.delete(id);
} catch (Exception e) {
log.error("delete错误!", e);
return ResultObject.failed(500, "服务端错误!请联系系统管理员!");
}
return ResultObject.success();
}
public ResultObject<Void> create(String name, String phone) {
try {
AdminUser adminUser = adminUserMapper.getAdminUserByPhone(phone);
if (adminUser != null) {
log.warn("手机号{}已被注册!", phone);
return ResultObject.failed("手机号已存在,请更换");
}
} catch (Exception e) {
log.error("根据手机号查询用户出错!", e);
return ResultObject.failed(500, "服务端错误!请联系系统管理员!");
}
try {
adminUserMapper.create(name, phone);
} catch (Exception e) {
log.error("注册用户报错!", e);
return ResultObject.failed(500, "服务端错误!请联系系统管理员!");
}
return ResultObject.success();
}
private static String getRedisCaptchaKey(String phone) {
return REDIS_KEY_SEND_CAPTCHA_TO_PREFIX + phone;
}
private static String getRedisCaptchaListKey(String phone) {
return REDIS_KEY_CAPTCHA + phone;
}
/**
* 生成随机六位数字验证码
*
* @return 数字验证码
*/
private static String randomCaptcha() {
StringBuilder stringBuilder = new StringBuilder();
Random random = new Random();
for (int i = 0; i < 6; i++) {
int nextInt = random.nextInt(10);
stringBuilder.append(nextInt);
}
return stringBuilder.toString();
}
private static String getLoginLockKey(String phone) {
return REDIS_LOGIN_LOCK_KEY + phone;
}
}

View File

@ -0,0 +1,22 @@
server:
port: 13579
oss:
ak: LTAI5t8z9QNdCG6b54mDgx8p
sk: F3M41hTgH8g99ZgVWyelj42825YbZM
roleArn: acs:ram::1647420045565932:role/ramoss
endPoint: oss-cn-hangzhou.aliyuncs.com
region: oss-cn-hangzhou
bucketName: lengfeng-test
baseUrl: https://lengfeng-test.oss-cn-hangzhou.aliyuncs.com
sts:
endPoint: sts.cn-hangzhou.aliyuncs.com
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/reference?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8&allowMultiQueries=true
username: root
password: Aa123456@
redis:
host: 127.0.0.1
port: 6379

64
core/pom.xml Normal file
View File

@ -0,0 +1,64 @@
<?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.jinrui</groupId>
<artifactId>reference</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>core</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<bouncycastle.version>1.70</bouncycastle.version>
<jwt.version>4.4.0</jwt.version>
</properties>
<dependencies>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcutil-jdk15on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,21 @@
package com.jinrui.reference.core;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 跨域访问支持
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH")
.maxAge(3600);
}
}

View File

@ -0,0 +1,27 @@
package com.jinrui.reference.core.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jinrui.reference.core.model.vo.ResultObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class BaseController {
private static final Logger log = LoggerFactory.getLogger(BaseController.class);
private final ObjectMapper objectMapper;
protected BaseController(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
protected void logResponse(ResultObject<?> resultObject) {
try {
String resultString = objectMapper.writeValueAsString(resultObject);
log.info("result: {}", resultString);
} catch (JsonProcessingException e) {
log.error("error in parsing result object into string!", e);
}
}
}

View File

@ -0,0 +1,72 @@
package com.jinrui.reference.core.model.vo;
import java.util.ArrayList;
import java.util.List;
/**
* 分页对象
*
* @param <T> 返回值类型
*/
public class PageObject<T> extends ResultObject<List<T>> {
/**
* 分页页码 默认从1开始
*/
private int current;
/**
* 分页长度 默认为10
*/
private int size;
/**
* 数据总数
*/
private int total;
public static <T> PageObject<T> empty(int page) {
PageObject<T> pageObject = new PageObject<>();
pageObject.setCode(200);
pageObject.setCurrent(page);
pageObject.setSize(0);
pageObject.setTotal(0);
pageObject.setData(new ArrayList<>());
return pageObject;
}
public static <T> PageObject<T> failedPage(int code, String message) {
PageObject<T> pageObject = new PageObject<>();
pageObject.setCode(code);
pageObject.setMsg(message);
return pageObject;
}
public static <T> PageObject<T> failedPage(String message) {
return failedPage(400, message);
}
public int getCurrent() {
return current;
}
public void setCurrent(int current) {
this.current = current;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
}

View File

@ -0,0 +1,94 @@
package com.jinrui.reference.core.model.vo;
/**
* 通用返回结构
*
* @param <T> 返回对象类型
*/
@SuppressWarnings("unused")
public class ResultObject<T> {
/**
* 状态码
*/
private int code;
/**
* 错误信息
*/
private String msg;
/**
* 返回对象
*/
private T data;
/**
* 查询请求成功返回
* @param data 请求返回值
* @return 返回结构体
* @param <T> 返回值类型
*/
public static <T> ResultObject<T> success(T data) {
ResultObject<T> resultObject = new ResultObject<>();
resultObject.code = 200;
resultObject.data = data;
return resultObject;
}
/**
* 修改请求成功返回
* @return 返回结构体
* @param <T> 返回值类型
*/
public static <T> ResultObject<T> success() {
return success(null);
}
/**
* 前端参数错误返回
* @param msg 错误信息
* @return 返回结构体
* @param <T> 返回值类型
*/
public static <T> ResultObject<T> failed(String msg) {
return failed(400, msg);
}
/**
* 自定义状态码错误返回
* @param msg 错误信息
* @return 返回结构体
* @param <T> 返回值类型
*/
public static <T> ResultObject<T> failed(int code, String msg) {
ResultObject<T> resultObject = new ResultObject<>();
resultObject.code = code;
resultObject.msg = msg;
return resultObject;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}

View File

@ -0,0 +1,105 @@
//package com.jinrui.reference.core.service;
//
//import com.auth0.jwt.JWT;
//import com.auth0.jwt.JWTCreator;
//import com.auth0.jwt.JWTVerifier;
//import com.auth0.jwt.algorithms.Algorithm;
//import com.auth0.jwt.exceptions.JWTVerificationException;
//import com.auth0.jwt.interfaces.Claim;
//import com.auth0.jwt.interfaces.DecodedJWT;
//
//import org.slf4j.Logger;
//import org.slf4j.LoggerFactory;
//import org.springframework.stereotype.Service;
//import org.springframework.util.StringUtils;
//
//@Service
//public class JwtService {
//
// private static final Logger log = LoggerFactory.getLogger(JwtService.class);
// private static final String SECRET = "R!E@F#E$R%E^N&C*E";
//
// /**
// * 管理后台根据用户对象生成JWT TOKEN
// * @param adminUser 管理后台用户对象
// * @return JWT TOKEN
// */
// public String generateToken(AdminUser adminUser) {
// Long id = adminUser.getId();
// String username = adminUser.getUsername();
// JWTCreator.Builder jwtBuilder = JWT.create();
// jwtBuilder.withClaim("username", username);
// jwtBuilder.withClaim("id", id);
// jwtBuilder.withClaim("timestamp", System.currentTimeMillis());
// return jwtBuilder.sign(Algorithm.HMAC256(SECRET));
// }
//
// /**
// * 微信小程序根据用户对象生成JWT TOKEN
// * @param miniUser 小程序用户对象
// * @return JWT TOKEN
// */
// public String generateToken(MiniUser miniUser) {
// Long id = miniUser.getId();
// String phone = miniUser.getPhone();
// JWTCreator.Builder jwtBuilder = JWT.create();
// jwtBuilder.withClaim("phone", phone);
// jwtBuilder.withClaim("id", id);
// jwtBuilder.withClaim("timestamp", System.currentTimeMillis());
// return jwtBuilder.sign(Algorithm.HMAC256(SECRET));
// }
//
// /**
// * 管理后台校验JWT TOKEN
// * @param token token
// * @return 校验结果
// */
// public AdminUser verify(String token) {
// if (!StringUtils.hasText(token)) {
// log.warn("管理后台token为空!");
// return null;
// }
// JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET))
// .withClaimPresence("username")
// .withClaimPresence("id")
// .withClaimPresence("timestamp")
// .build();
// try {
// DecodedJWT decodedJWT = jwtVerifier.verify(token);
// Claim claim = decodedJWT.getClaim("id");
// Long adminUserId = claim.asLong();
// log.info("JWT解析成功管理后台用户ID: {}", adminUserId);
// return adminUserMapper.getById(adminUserId);
// } catch (JWTVerificationException e) {
// log.error("JWT解析异常token: {}", token, e);
// return null;
// }
// }
//
// /**
// * 小程序校验JWT TOKEN
// * @param token token
// * @return 校验结果
// */
// public MiniUser verifyMini(String token) {
// if (!StringUtils.hasText(token)) {
// log.warn("小程序token为空!");
// return null;
// }
// JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET))
// .withClaimPresence("phone")
// .withClaimPresence("id")
// .withClaimPresence("timestamp")
// .build();
// try {
// DecodedJWT decodedJWT = jwtVerifier.verify(token);
// Claim claim = decodedJWT.getClaim("id");
// Long miniUserId = claim.asLong();
// log.info("JWT解析成功小程序用户ID: {}", miniUserId);
// return miniUserMapper.getById(miniUserId);
// } catch (JWTVerificationException e) {
// log.error("JWT解析异常token: {}", token, e);
// return null;
// }
// }
//}

20
mini/pom.xml Normal file
View File

@ -0,0 +1,20 @@
<?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.jinrui</groupId>
<artifactId>reference</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>mini</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

87
pom.xml Normal file
View File

@ -0,0 +1,87 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<modules>
<module>admin</module>
<module>mini</module>
<module>core</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mybatis-version>2.3.2</mybatis-version>
</properties>
<groupId>com.jinrui</groupId>
<artifactId>reference</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>reference</name>
<repositories>
<repository>
<id>hejiuxiaofendui-flexible-wesley</id>
<name>wesley</name>
<url>https://hejiuxiaofendui-maven.pkg.coding.net/repository/flexible/wesley/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<phase>compile</phase>
</execution>
</executions>
<configuration>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
<configuration>
<testFailureIgnore>true</testFailureIgnore>
</configuration>
</plugin>
</plugins>
</build>
</project>