一:用户界面
全局处理异常
统一了返回前端的数据类型以及返回信息
public class Result<T> {
private Integer code;//业务状态码 0-成功 1-失败
private String message;//提示信息
private T data;//响应数据
//快速返回操作成功响应结果(带响应数据)
public static <E> Result<E> success(E data) {
return new Result<>(0, "操作成功", data);
}
//快速返回操作成功响应结果
public static Result success() {
return new Result(0, "操作成功", null);
}
public static Result error(String message) {
return new Result(1, message, null);
}
}
ThreadLocal工具类实现代码
@SuppressWarnings("all")
public class ThreadLocalUtil {
//提供ThreadLocal对象,
private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();
//根据键获取值
public static <T> T get(){
return (T) THREAD_LOCAL.get();
}
//存储键值对
public static void set(Object value){
THREAD_LOCAL.set(value);
}
//清除ThreadLocal 防止内存泄漏
public static void remove(){
THREAD_LOCAL.remove();
}
}
生成token工具类
public class JwtUtil {
private static final String KEY = "Authorization";
//接收业务数据,生成token并返回
public static String genToken(Map<String, Object> claims) {
// .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12)) token有效时长
return JWT.create()
.withClaim("claims", claims)
.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12))
.sign(Algorithm.HMAC256(KEY));
}
//接收token,验证token,并返回业务数据
public static Map<String, Object> parseToken(String token) {
return JWT.require(Algorithm.HMAC256(KEY))
.build()
.verify(token)
.getClaim("claims")
.asMap();
}
}
1:注册业务
首先建立controller、service、mapper、 三个模块
在controller中编写UserController类
在mapper中建立UserMapper类
在Service中建立UserService类
实现逻辑:通过查询用户名查看是否可以注册,如数据库中存在则不可注册,反之可以
通过controller去调用service,service调用mapper.xml中的sql语句去查询用户名是否存在,若存在返回提示不可注册,若不存在则可以注册
重点:
<!-- validation依赖 完成参数校验-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
引入该依赖完成参数的校验
引入MD5对用户密码加密
public class Md5Util {
/**
* 默认的密码字符串组合,用来将字节转换成 16 进制表示的字符,apache校验下载的文件的正确性用的就是默认的这个组合
*/
protected static char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
protected static MessageDigest messagedigest = null;
static {
try {
messagedigest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException nsaex) {
System.err.println(Md5Util.class.getName() + "初始化失败,MessageDigest不支持MD5Util。");
nsaex.printStackTrace();
}
}
/**
* 生成字符串的md5校验值
*
* @param s
* @return
*/
public static String getMD5String(String s) {
return getMD5String(s.getBytes());
}
/**
* 判断字符串的md5校验码是否与一个已知的md5码相匹配
*
* @param password 要校验的字符串
* @param md5PwdStr 已知的md5校验码
* @return
*/
public static boolean checkPassword(String password, String md5PwdStr) {
String s = getMD5String(password);
return s.equals(md5PwdStr);
}
public static String getMD5String(byte[] bytes) {
messagedigest.update(bytes);
return bufferToHex(messagedigest.digest());
}
private static String bufferToHex(byte bytes[]) {
return bufferToHex(bytes, 0, bytes.length);
}
private static String bufferToHex(byte bytes[], int m, int n) {
StringBuffer stringbuffer = new StringBuffer(2 * n);
int k = m + n;
for (int l = m; l < k; l++) {
appendHexPair(bytes[l], stringbuffer);
}
return stringbuffer.toString();
}
private static void appendHexPair(byte bt, StringBuffer stringbuffer) {
char c0 = hexDigits[(bt & 0xf0) >> 4];// 取字节中高 4 位的数字转换, >>>
// 为逻辑右移,将符号位一起右移,此处未发现两种符号有何不同
char c1 = hexDigits[bt & 0xf];// 取字节中低 4 位的数字转换
stringbuffer.append(c0);
stringbuffer.append(c1);
}
}
在service中对用户的密码进行加密操作
2:登录业务
实现逻辑:通过输入的账户密码去数据库中查找,若存在则登录成功,否则登录失败
重点:
<!-- jwt坐标--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.3.0</version> </dependency> <!-- springboot的单元测试 把junit整合了--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency>
引入该依赖添加jwt登录验证,保证系统的安全性
JWT生成token
JWT生成token的工具类
public class JwtUtil {
private static final String KEY = "Authorization";
//接收业务数据,生成token并返回
public static String genToken(Map<String, Object> claims) {
return JWT.create()
.withClaim("claims", claims)
.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12))
.sign(Algorithm.HMAC256(KEY));
}
//接收token,验证token,并返回业务数据
public static Map<String, Object> parseToken(String token) {
return JWT.require(Algorithm.HMAC256(KEY))
.build()
.verify(token)
.getClaim("claims")
.asMap();
}
}
用户进行登陆时使用JWT生成token
@PostMapping("/login")
public Result<String> login(@Pattern(regexp = "^\\S{5,16}$") String username,@Pattern(regexp = "^\\S{5,16}$")String password){
//根据用户名查询用户
User loginName = userService.findByUserName(username);
//判断用户是否存在
if (loginName==null){
return Result.error("用户名错误");
}
//判断密码是否正确
if (Md5Util.getMD5String(password).equals(loginName.getPassword())){
//生成登录的token
Map<String,Object>ztz = new HashMap<>();
ztz.put("id",loginName.getId());
ztz.put("user",loginName.getUsername());
String token = JwtUtil.genToken(ztz);
return Result.success(token);
}
return Result.error("密码错误");
}
拦截器
如果不加拦截器,用户操作时可以不进行登录即可访问其他资源
拦截器需要实现HandlerInterceptor接口中的preHandle方法之后在对拦截器进行注册
@Component
public class LoginHeandler implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//令牌验证
String token = request.getHeader("Authorization");
try {
Map<String, Object> stringObjectMap = JwtUtil.parseToken(token);
/**
* preHandle=true 拦截器的放行
*/
System.out.println("进入了拦截器指令");
return true;
} catch (Exception e) {
//设置响应码为401
response.setStatus(401);
/**
* 拦截器的不放行
*/
System.err.println("拦截器没有放行");
return false;
}
}
}
注册拦截器需实现WebMvcConfigurer接口
对loginhandler拦截器进行注册才可使用
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginHeandler loginHeandler;
@Override
public void addInterceptors(InterceptorRegistry registry) {
/**
* 登录和注册不进行拦截
*/
registry.addInterceptor(loginHeandler).excludePathPatterns("/user/login","/user/register");
}
}
目前该项目结构 2023/11/21
3:获取用户详细信息
首先根据用户名查询用户
@GetMapping("/userinfo")
public Result<User> userinfo(){
//根据用户名查询用户
// Map<String, Object> Map = JwtUtil.parseToken(token);
// String username = (String) Map.get("user");
Map<String,Object> map = ThreadLocalUtil.get();
String username = (String) map.get("user");
User user = userService.findByUserName(username);
return Result.success(user);
}
重点
ThreadLocal 线程安全
新建一个ThreadLocal对象可多个用户访问 且每个用户会独立开启一个局部变量 仅供当前访问者使用
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
因为在后期操作数据是需要使用到用户的id信息,所以可以使用ThreadLocal来保存当前用户的id和name属性以供后期访问该数据
用户的登录生成的token中存在用户的id以及name所以可以解析token来查看id以及name
每个请求都需要进入拦截器所以可以在此使用ThreadLocal存储当前用户的id以及name
public class LoginHeandler implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//令牌验证
String token = request.getHeader("Authorization");
try {
Map<String, Object> stringObjectMap = JwtUtil.parseToken(token);
/**
* preHandle=true 拦截器的放行
*/
System.out.println("拦截器放行了");
//把业务数据存到ThreadLocal中
ThreadLocalUtil.set(stringObjectMap);
return true;
} catch (Exception e) {
//设置响应码为401
response.setStatus(401);
/**
* 拦截器的不放行
*/
System.err.println("拦截器没有放行");
return false;
}
}
修改信息
可通过以下注解来实现参数校验
二:文章界面
1:分组校验
重点参数校验中的分组校验 (两个不同请求一个需要id一个不需要id但实体类中id标注为非空,所以不需要id的那个请求就会报错无法执行,这时候就需要我们的分组校验了)
public class Category {
@NotNull(groups = update.class)
private Integer id;//主键ID
@NotEmpty
private String categoryName;//分类名称
@NotEmpty
private String categoryAlias;//分类别名
private Integer createUser;//创建人ID
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;//创建时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;//更新时间
//如果说某个校验项没有指定分组,默认属于Default分组
//分组之间可以继承, A extends B 那么A中拥有B中所有的校验项
public interface add extends Default {
}
public interface update extends Default{
}
2:自定义注解实现二选一
文章的发布状态只能是已发布或是草稿 只能在这两种值中选择一个所以就需要我们的自定义注解了 通常自定义注解通过以下三部完成
1(模板可以点击@NotEmpty进入底层去复制修改)
2
3 使用自定义的注解实现状态二选一
3:实现分页
引入pom依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
编写分页配置类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean <T>{
private Long total;//总条数
private List<T> items;//当前页数据集合
}
@RequestParam(required = false) Integer categoryId == 可以没有该参数
服务实现
数据层为动态sql
<mapper namespace="com.ze.Mapper.ArticleMapper">
<!-- 动态sql-->
<select id="list" resultType="com.ze.Pojo.Article">
select * from big_event.article
<where>
<if test="categoryId!=null">
category_id =#{categoryId}
</if>
<if test="state!=null">
and state=#{state}
</if>
and create_user=#{id}
</where>
</select>
</mapper>
三:文件上传
文件上传本人用的是阿里云oss
文档资料
使用阿里云OSS实现图片文件上传_oss上传图片_何中应的博客-CSDN博客
阿里云上传文件工具类
ACCESS_KEY_ID、ACCESS_KEY_SECRET:开通阿里云oss之后自动生成(记得保存)
BUCKET_NAME:进行存储的命名空间(自己可以定义)
ENDPOINT:代理节点的全称
package com.ze.Utils;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import java.io.FileInputStream;
import java.io.InputStream;
public class AliOSSUtils {
private static final String ENDPOINT = "https://oss-cn-beijing.aliyuncs.com";
private static final String ACCESS_KEY_ID = "LTAI5tQtZsyafFwcAHKco63P";
private static final String ACCESS_KEY_SECRET = "iBFHWp35bPnAjOT4YIk6ktIxv64QSM";
private static final String BUCKET_NAME = "zkvcawk";
public static String UploadFile(String objname, InputStream in) throws Exception {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
// String endpoint = "https://oss-cn-beijing.aliyuncs.com";
// // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
// // EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
// String ACCESS_KEY_ID = "LTAI5tQtZsyafFwcAHKco63P";
// String ACCESS_KEY_SECRET = "iBFHWp35bPnAjOT4YIk6ktIxv64QSM";
// 填写Bucket名称,例如examplebucket。
// String bucketName = "zkvcawk";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
// String objectName = "001.png";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
String url = "";
try {
// 填写字符串。
String content = "Hello OSS,你好世界";
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, objname, in);
// 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
// ObjectMetadata metadata = new ObjectMetadata();
// metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
// metadata.setObjectAcl(CannedAccessControlList.Private);
// putObjectRequest.setMetadata(metadata);
// 上传字符串。
PutObjectResult result = ossClient.putObject(putObjectRequest);
url = "https://"+BUCKET_NAME+"."+ENDPOINT.substring(ENDPOINT.lastIndexOf("/")+1)+"/"+objname;
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return url;
}
}
UUID
如果上传的文件名有相同的那么就会产生覆盖情况这时候就需要是使用到uuid来确保每个上传文件都有唯一的id
到这一步文件就是上传成功了需要把文件的url地址返回去
url=上面四个的拼接
四:redis优化登录
如不进行优化的话,用户修改密码之后会生成一个新的token,但是如果不使用redis优化,会出现可以使用旧密码的token来访问资源
首先需要在yml文件中加入配置项
引入pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
相关代码
reids是需要使用spring容器的所以使用前必须完成自动装配
用户登录成功之后同时把token存到redis中
在拦截器中获取redis中的token判断 redis中token失效时间与创建时失效时间保持一致
修改密码逻辑
持续更新中......