html:
input
<input type="date">:日期 <input type="datetime">:日期时间(兼容性不是很好) <input type="datetime-local" />日期时间 <input type="time" />时间
outline 可去掉框
<textarea>文本域显示的大小
上传资料:<input type="file" />
Framset:不能和body同时出现,、
Framset标签的作用,使当前页面进行其他页面内容的显示设置,页面本身的body标签失效;有body无框架,有框架无body;
常用属性:rows对多个页面进行“行”布局
Cols对多个页面进行“列”布局
Eg:<frameset rows="30%,*" frameborder="0">
行内样式 在标签里面写
外接样式 在<style>标签里面写
属性选择器 指定元素的属性【属性=属性值】
Id > class > tagname(标记选择器的优先级最低)
f
flex布局
display: flex; 父元素设置 子块元素会排成一行
align-items: center;竖着的
justify-content: center; 横着的
子块元素左右对齐:子块元素设置 margin-?:auto
子元素都设置flex:1 会在一行平分flex:0 1 10% 挨个设置 会按比例占据位置。
设置布局!
.parent { display: flex; padding: 20px; } .child { width: 100%; } Flexbox会自动处理尺寸的计算
现在依旧会溢出,必须加上:
* { padding: 0; margin: 0; box-sizing: border-box; /* 这样设置可以避免大部分宽度计算问题 */ }
padding 内边距 上右下左 margin 外边距 想让他上移 margin设置负数
gap : 设置框与框的 间距 gap:20px
flex-wrap:wrap 换行。
文字撑满容器:
text-align: justify;(快淘汰了)
text-align:center
不换行
- overflow:hidden 隐藏
- white-space:normal 默认
- pre 换行和其他空白字符都将受到保护
- nowrap(在一行显示)
z -index: 当 position不是static时
居中对齐!
水平 text-align:
垂直 设置行高=容器高度 line-height: 60px;
居于最中间:
/* 设置元素宽度和高度 */ width: 800px; height: 500px; /* 将元素定位为绝对定位,相对于其最近的非静态定位的父元素进行定位 */ position: absolute; /* 将元素的左上角定位在其父元素的50%水平位置 */ left: 50%; /* 将元素的左上角定位在其父元素的50%垂直位置 */ top: 50%; /* 通过负的 margin-left 值,使元素的中心点在水平方向上向左移动,实现水平居中 */ margin-left: -400px; /* 通过负的 margin-top 值,使元素的中心点在垂直方向上向上移动,实现垂直居中 */ margin-top: -250px;
Jscript页面传参
写到storage中
接受其他页面传递的数据
setInterval(
() => {
hh.height = Number(localStorage.getItem("hhh"))+Number(10)
console.log(hh.height)
}, 100)传递页面参数
let my = document.getElementById("box");
window.onload = function() {
localStorage.setItem("hhh",my.scrollHeight)
}
iframe的间距如何去除?
align="top"
js script放在 <html/>标签后会出现bug
id 写在body中 不能写在 html中
display: inline-block; 要对 每一个子块元素设置!
flex最后一个元素右对齐
“在使用display:flex的前提下,对项目的最后一个元素进行向右对齐,则使用margin-left:auto(在最后一个元素)即可。
获取时间 function aa() { var time = new Date() var oldtime = time.getTime() //获取1970年到现在的毫秒数 var time2 = new Date('2024/3/12 18:00:00') var newtime = time2.getTime() //获取1970到2024的毫秒数 var end = Math.floor((newtime - oldtime) / 1000) // 现在到2024的秒数 console.log("总秒数:", end) var day = Math.floor(end / 86400); //总秒数/一天的秒数==几天(保留整数) console.log("总天数:", day) end = Math.floor(end % 86400) //剩下的秒数 console.log("剩下的秒数:", end) var hour = Math.floor(end / 3600) console.log("小时数:", hour) var end = Math.floor(end % 3600) var min = Math.floor(end / 60) console.log("分钟数:", min) var second = Math.floor(end % 60) console.log("秒数:", second) //写到html页面上 document.getElementById("second").innerText = second document.getElementById("day").innerText = day document.getElementById("hour").innerText = hour document.getElementById("min").innerText = min }
<datalist> 标签定义选项列表。请与 input 元素配合使用该元素,来定义 input 可能的值。
datalist 及其选项不会被显示出来,它仅仅是合法的输入值列表。
表单提交的数据 要设置name属性
下面的可以访问上面的!!!
设置position:fixd后 高宽度就失效了 就飘起来了 再设置个空盒子 占据它的位置
js实现 同意协议允许注册
function a() { var dot = document.getElementById("tongyi") var button = document.getElementById("zhuce") if (dot.checked === true) { button.removeAttribute("disabled") console.log("www") button.style.background = "red" } else { button.disabled = "disabled"; button.style.background = "gray" } } function b(event) { var dot = document.getElementById("tongyi") if (dot.checked != true) { console.log("哈哈哈") event.preventDefault() window.confirm("please agree the items") } }
js调用第三方接口
放网址 它会给咱传入参数 json 注意格式转化。
fetch.then是异步操作,里面的操作是无顺序的,所以要再加个.then();
// 2 查询天气 function weather(id) { fetch(`https://devapi.qweather.com/v7/weather/now?key=8431c4eef589430284f12e43b6517ec6&location=${id}`).then(res => { return res.text() }).then(data => { var obj = JSON.parse(data) // 获取温度 var wd = document.querySelector(".wd h1 span:nth-child(1)") // 将温度赋值给页面 wd.innerHTML = obj.now.temp // 获取天气图标 var img = document.querySelector(".wd h1 img") // 设置图片的地址 img.src = `icon/${obj.now.icon}.png` // 获取设置天气描述数据 var q = document.querySelector(".qing") q.innerHTML = obj.now.text // 获取设置湿度数据 var shidu = document.querySelector(".shidu") shidu.innerHTML = obj.now.humidity + "%" // 获取设置风向数据 var fengxiang = document.querySelector(".fengxiang") fengxiang.innerHTML = obj.now.windDir // 获取设置风速数据 var dengji = document.querySelector(".dengji") dengji.innerHTML = obj.now.windScale + "级" })
后端
Mapper
BaseMapper的作用?为什么要继承它
/** * BaseMapper 概述 * * BaseMapper 接口定义了一组通用的方法,可以直接在 Mapper 接口中使用。这些方法包括: */ /** * Mapper接口 */ @Mapper public interface RoleMapper extends BaseMapper<Role> { } // Service类 @Service public class RoleService { @Autowired private RoleMapper roleMapper; /** * 插入一条记录 */ public void insertRole(Role role) { roleMapper.insert(role); } /** * 根据ID删除一条记录 */ public void deleteRoleById(Long id) { roleMapper.deleteById(id); } /** * 根据ID集合批量删除记录 */ public void deleteRolesByIds(List<Long> ids) { roleMapper.deleteBatchIds(ids); } /** * 根据ID更新一条记录 */ public void updateRoleById(Role role) { roleMapper.updateById(role); } /** * 根据ID查询一条记录 */ public Role selectRoleById(Long id) { return roleMapper.selectById(id); } /** * 根据ID集合批量查询记录 */ public List<Role> selectRolesByIds(List<Long> ids) { return roleMapper.selectBatchIds(ids); } /** * 根据条件查询记录列表 */ public List<Role> selectRolesByRoleKey(String roleKey) { QueryWrapper<Role> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("role_key", roleKey); return roleMapper.selectList(queryWrapper); } /** * 根据条件查询记录数量 */ public int selectRoleCountByRoleKey(String roleKey) { QueryWrapper<Role> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("role_key", roleKey); return roleMapper.selectCount(queryWrapper); } /** * 根据条件查询单条记录 */ public Role selectOneRoleByRoleKey(String roleKey) { QueryWrapper<Role> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("role_key", roleKey); return roleMapper.selectOne(queryWrapper); } /** * 根据条件分页查询记录 */ public IPage<Role> selectRolePage(int pageNum, int pageSize, String roleKey) { Page<Role> page = new Page<>(pageNum, pageSize); QueryWrapper<Role> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("role_key", roleKey); return roleMapper.selectPage(page, queryWrapper); } }
QueryWrapper<Role> queryWrapper=new QueryWrapper<>();//QueryWrapper用来做比较的! queryWrapper.eq("role_key","admin");//eq相等 ge大于等于 等等,具体看baomidou官网
代码生成
Mybatis-PlusX 代码生成后还要在 主启动类中加入 包扫描,这样才能正确运行!必选
@MapperScan("com.ccz.vueshop.mapper")
SQL打印用 p6spy
<dependency> <groupId>p6spy</groupId> <artifactId>p6spy</artifactId> <version>3.9.1</version> </dependency>
spring: profiles: active: dev
注意测试环境使用。application-dev
需引入spy.properties文件
################################################################# # P6Spy 配置文件 # # 详细使用说明请参考 # # http://p6spy.github.io/p6spy/2.0/configandusage.html # ################################################################# ################################################################# # 模块 # # # # modulelist 指定了P6Spy的模块功能。 # # 只有列出的模块才会被激活。 # # (默认是 com.p6spy.engine.logging.P6LogFactory 和 # # com.p6spy.engine.spy.P6SpyFactory) # # 请注意,核心模块 (P6SpyFactory) 不能被禁用。 # # 与其他属性不同,这一属性的变化需要重新加载才能生效。 # ################################################################# modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory # 如果使用 3.2.1 之前的版本 # modulelist=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory # 自定义日志格式输出 logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger # 日志输出到控制台 appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger # 使用日志系统记录 SQL # appender=com.p6spy.engine.spy.appender.Slf4JLogger # 启用 p6spy driver 代理 deregisterdrivers=true # 使用带前缀的 JDBC URL useprefix=true # 配置日志记录的类别,去除不必要的类别,如:error, info, batch, debug, statement, commit, rollback, result, resultset excludecategories=info,debug,result,commit,resultset # 日期格式 dateformat=yyyy-MM-dd HH:mm:ss # 指定要加载的 JDBC 驱动程序列表 # driverlist=org.h2.Driver # 是否启用SQL长时间未响应记录 outagedetection=true # SQL长时间未响应的标准(单位:秒) outagedetectioninterval=2
相应修改
spring: datasource: driver-class-name: com.p6spy.engine.spy.P6SpyDriver url: jdbc:p6spy:mysql://localhost:3306/vueshop?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 1234
结果集封装
前后端交互的标准
package com.markerhub.common.lang; import lombok.Data; import java.io.Serializable; @Data public class Result implements Serializable { // 结果状态(如200表示成功,400表示异常) private int code; // 结果消息 private String msg; // 结果数据 private Object data; public static Result success() { return success(null); } public static Result success(Object data) { Result result = new Result(); result.setCode(200); result.setMsg("操作成功"); result.setData(data); return result; } public static Result fail(String msg) { Result result = new Result(); result.setCode(400); result.setMsg(msg); result.setData(null); return result; } }
全局异常处理
不可避免服务器报错的情况,如果不配置异常处理机制,就会默认返回tomcat或者nginx的5XX页面,对普通用户来说
404全局异常处理:
mvc: throw-exception-if-no-handler-found: true web: resources: add-mappings: false
Hibernate validatior 实体校验
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
@TableName("app_user") public class AppUser implements Serializable { @TableId(value = "id", type = IdType.AUTO) private Long id; @NotBlank(message = "昵称不能为空") private String username; @NotBlank(message = "手机号不能为空") private String tel; ... }
/** * 测试实体校验 * @param user * @return */ @PostMapping("/save") public Object testSave(@Validated @RequestBody AppUser appUser) { return user.toString(); }
异常捕获
// 设置当抛出 BindException 异常时返回的 HTTP 状态码为 400 Bad Request @ResponseStatus(HttpStatus.BAD_REQUEST) // 定义一个方法来处理 BindException 异常 @ExceptionHandler(value = BindException.class) public Result handler(BindException e) { // 获取绑定结果(验证结果) BindingResult bindingResult = e.getBindingResult(); // 从绑定结果中获取第一个错误对象 ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get(); // 记录错误日志,包含错误信息 log.error("实体校验异常 ---- {}", objectError.getDefaultMessage()); // 返回失败结果,包含错误信息 return Result.fail(objectError.getDefaultMessage()); }
跨域问题
前后端域名不一样就出现了问题了,限定同域名才可以访问,所以要让它没有这个限制 很固定!在Config里面配置即可。
package com.markerhub.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addExposedHeader("Authorization");
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
// .allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
}
JWT登录验证
1.导入包 配置
<!-- jwt --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!--redis存登录信息--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- 实体验证--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- hutool工具类--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.12</version> </dependency> <!-- BouncyCastle 是一个提供加密算法和协议的开源库,只有加了这个Hutool的工具类才能正常加密!--> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> </dependency>
jwt: secret: f4e2e52034348f86b67cde581c0f9eb5 expire: 604800
secret
是一个字符串,用作 JWT 的签名密钥。
expire
是一个长整型数值,表示 JWT 的过期时间,单位为秒.604800 秒(即 7 天)
2.JwtUtils
jwt工具类 格式很固定,copy此版本即可。
package com.ccz.vueshop.utils; // 定义了包名,用于组织和管理相关的类文件。 import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; // 导入 JSON Web Token (JWT) 库中的类,用于处理 JWT。 import lombok.Data; import lombok.extern.slf4j.Slf4j; // 导入 Lombok 库中的注解,简化代码,生成日志记录器和 getter/setter 方法。 import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; // 导入 Spring 框架中的注解,用于配置类和声明组件。 import java.util.Date; // 导入 Java 标准库中的 Date 类,用于处理日期和时间。 @Slf4j // 为类启用日志记录功能。 @Data // 生成 getter/setter 方法、toString 方法、equals 和 hashCode 方法。 @Component // 将此类声明为 Spring 组件,使其可以被自动扫描和装配。 @ConfigurationProperties(prefix = "jwt") // 将此类的属性与配置文件中的 "jwt" 前缀的属性进行绑定。 public class JwtUtils { private String secret; // JWT 签名的密钥,从配置文件中获取。 private long expire; // JWT 的过期时间,从配置文件中获取,单位为秒。 /** * 生成 JWT token */ public String generateToken(long userId, String issuer) { Date nowDate = new Date(); // 获取当前时间。 // 过期时间 Date expireDate = new Date(nowDate.getTime() + expire * 1000); // 计算过期时间,当前时间加上配置的过期时长。 return Jwts.builder() .setHeaderParam("typ", "JWT") // 设置头部参数,类型为 JWT。 .setSubject(userId + "") // 设置主题,即用户ID。 .setIssuedAt(nowDate) // 设置签发时间。 .setIssuer(issuer) // 设置签发者。 .setExpiration(expireDate) // 设置过期时间。 .signWith(SignatureAlgorithm.HS512, secret) // 设置签名算法和密钥。 .compact(); // 生成并压缩 JWT。 } /** * 从 JWT token 中获取 Claims */ public Claims getClaimByToken(String token, String issuer) { try { Claims claims = Jwts.parser() .setSigningKey(secret) // 设置解析 JWT 的签名密钥。 .parseClaimsJws(token) // 解析 JWT。 .getBody(); // 获取其中的 Claims。 if (issuer == null || !claims.getIssuer().equals(issuer)) { // 验证签发者是否一致。 return null; } return claims; // 返回 Claims。 } catch (Exception e) { log.debug("validate is token error ", e); // 记录验证 token 的错误信息。 return null; } } /** * 判断 token 是否过期 * @return true:过期 */ public boolean isTokenExpired(Date expiration) { return expiration.before(new Date()); // 判断过期时间是否在当前时间之前,若是则表示 token 已过期。 } }
- Date:一个较老的类,包含日期和时间,精确到毫秒,不推荐在新代码中使用。
- LocalDateTime:Java 8 引入的类,表示不带时区的日期和时间,精确到纳秒,推荐使用。
- JSR310: Date APl 充分利用 Java 8 时间 API 的优势,包括更好的设计、丰富的功能和线程安全性。
3.业务代码
3.1 Service层代码
@Override public AppUser login(LoginDto loginDto) { // 根据用户名查询用户信息 AppUser user = this.getOne(new QueryWrapper<AppUser>().eq("username", loginDto.getUsername())); // 断言用户不为空,如果为空则抛出异常,提示用户名或密码错误 Assert.notNull(user, "用户名或密码错误"); // 输出登录请求中的加密密码(用于调试) System.out.println("加密密码:" + SecureUtil.md5(loginDto.getPassword())); // 验证密码是否正确,如果不正确则抛出异常 if (!user.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))) { throw new HubException("用户名或密码错误"); } // 更新用户的最后登录时间为当前时间 user.setLastLogin(LocalDateTime.now()); // 根据用户 ID 更新用户信息, //this:当前类的实例。在这个上下文中,this 表示调用 login 方法的当前对象 this.updateById(user); // 返回用户的 ID return user; }
3.2Controller 层代码
@PostMapping("/login") public Result login(@Validated @RequestBody LoginDto loginDto) { // 校验用户名和密码 AppUser appUser = appUserService.login(loginDto); // 生成 token String token = jwtUtils.generateToken(appUser.getId(), "APP"); // 返回成功结果,包含 token 和用户信息 return Result.success(MapUtil.builder() .put("token", token) .put("userInfo", appUser) .build()); }
注册
导入
<!--图片验证码--> <dependency> <groupId>com.github.axet</groupId> <artifactId>kaptcha</artifactId> <version>0.0.9</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
配置信息:
/** * 图片验证码的配置信息 */ @Configuration public class KaptchaConfig { @Bean public DefaultKaptcha producer() { Properties properties = new Properties(); properties.put("kaptcha.border", "no"); properties.put("kaptcha.textproducer.font.color", "black"); properties.put("kaptcha.textproducer.char.space", "4"); properties.put("kaptcha.image.height", "40"); properties.put("kaptcha.image.width", "120"); properties.put("kaptcha.textproducer.font.size", "30"); Config config = new Config(properties); DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); defaultKaptcha.setConfig(config); return defaultKaptcha; } }
生成验证码 通过谷歌的工具生成的
@GetMapping("/captcha") // 注解:将该方法映射为处理 HTTP GET 请求的处理器,URL 路径为 "/captcha"。 public Result getCaptcha() throws IOException { // 方法定义:返回类型为 Result,方法名为 getCaptcha,抛出 IOException 异常。 // 生成uuid和验证码 String uuid = UUID.randomUUID().toString(); // 生成一个唯一的 UUID 字符串,用作验证码的唯一标识符。 String code = producer.createText(); // 生成验证码文本,通常是一个随机生成的字符串。 log.info("uuid - code - {},{}", uuid, code); // 记录日志:打印生成的 UUID 和验证码,用于调试和跟踪。 // 生成base64编码的图片验证码 BufferedImage image = producer.createImage(code); // 根据验证码文本生成验证码图片。 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); // 创建一个字节数组输出流,用于存储图片数据。 ImageIO.write(image, "jpeg", outputStream); // 将验证码图片以 JPEG 格式写入字节数组输出流。 BASE64Encoder encoder = new BASE64Encoder(); // 创建一个 Base64 编码器,用于将图片数据转换为 Base64 编码字符串。 String base64Img = "data:image/jpeg;base64," + encoder.encode(outputStream.toByteArray()); // 将图片数据转换为 Base64 编码字符串,并添加 MIME 类型前缀。 // 把uuid和验证码存储到redis redisUtil.set(uuid, code, 120); // 将 UUID 和验证码文本存储到 Redis,设置有效期为 120 秒。 // 返回uuid和base64图片 return Result.success(MapUtil.builder() .put("uuid", uuid) .put("base64Img", base64Img) .build()); // 返回结果:包含 UUID 和 Base64 编码的图片验证码的成功响应。 }
RegisterDto是为了接受前端来的json数据,以及校验,不能用 实体类 ,为了职责分离和降低耦合。同时用了实体校验。
@Data public class RegisterDto implements Serializable { @NotBlank(message="昵称不能为空") private String username; @NotBlank(message="密码不能为空") private String password; @NotBlank(message="验证码错误") private String uuid; @NotBlank(message="验证码错误") private String code; }
现在有了验证码了 进行注册!
@PostMapping("/register") // 注解:将该方法映射为处理 HTTP POST 请求的处理器,URL 路径为 "/register"。 public Result register(@Validated @RequestBody RegisterDto form){ // 方法定义:返回类型为 Result,方法名为 register,使用 @RequestBody 注解将请求体中的 JSON 数据映射为 RegisterDto 对象,并使用 @Validated 注解进行验证。 // 验证码校验 Assert.isTrue(form.getCode().equals(redisUtil.get(form.getUuid())), "验证码错误"); // 检查验证码是否正确。使用 UUID 获取 Redis 中存储的验证码,并与表单中的验证码进行比较。如果不匹配,抛出异常。 long count = appUserService.count(new QueryWrapper<AppUser>().eq("username", form.getUsername())); // 查询数据库中是否已有相同用户名的用户。使用 MyBatis Plus 的 QueryWrapper 构造查询条件。 if (count > 0){ return Result.fail("用户名已存在"); } // 如果数据库中已存在相同用户名的用户,则返回失败结果,提示用户名已存在。 AppUser user = new AppUser(); // 创建一个新的 AppUser 实例。 user.setUsername(form.getUsername()); // 设置用户的用户名为表单中的用户名。 user.setPassword(SecureUtil.md5(form.getPassword())); // 设置用户的密码为表单中的密码,并使用 MD5 加密。 user.setCreated(LocalDateTime.now()); // 设置用户的创建时间为当前时间。 appUserService.save(user); // 将用户信息保存到数据库中。 log.info("用户注册成功 ---- {}", user.getUsername()); // 记录日志,提示用户注册成功,并打印用户名。 return Result.success(); // 返回成功结果。 }
用户认证Token
登录完成之后,就会返回系统生成的jwt为了身份认证的token,在访问受限资源的时候需要在请求头header中携带token作为身份认证,比如访问用户中心,访问购物车,查看我的订单。这些接口都是需要知道当前用户是谁才能访问的。
app端和system端不一样,权限系统有shiro框架。在App端,我们如何让当前接口设置只有登录用户才能访问呢?这次的解决方案也很简单,我们定义一个@Login注解,在需要登录才能访问的接口添加上这个注解,然后设置一个拦截器拦截这个注解。拦截器中获取用户的jwt,判断是否合法,然后把userId存储到session中,这样在当前会话的操作中都能知道用户是谁了。
<!-- 添加 Apache Shiro 依赖--> <!-- 处理身份验证、授权、加密和会话管理等安全相关任务--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.9.1</version> </dependency>
1.定义注解
@Target(ElementType.METHOD) // 指定该注解只能用于方法上。 @Retention(RetentionPolicy.RUNTIME) // 指定该注解在运行时可见。 @Documented // 使该注解包含在 Javadoc 中。 public @interface Login { // 定义一个名为 Login 的注解,没有任何属性。 }
拦截器的逻辑其实很简单:
- 判断访问的方法上是否有Login注解,没有就放行
- 有token的话就验证jwt是否合法
- 把userId设置在session中,方便后续流程中需要获取用户Id
然后还需要把拦截器注入到spring框架中,拦截所有/app开头的链接package com.ccz.vueshop.modules.app.interceptor; import com.ccz.vueshop.common.lang.Const; import com.ccz.vueshop.modules.app.annotation.Login; import com.ccz.vueshop.utils.JwtUtils; import io.jsonwebtoken.Claims; import org.apache.shiro.authz.UnauthenticatedException; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Component public class AuthInterceptor implements HandlerInterceptor { @Resource JwtUtils jwtUtils; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 判断是否有@Login注解,没有就放行 Login annotaion; if (handler instanceof HandlerMethod) { annotaion = ((HandlerMethod) handler).getMethodAnnotation(Login.class); } else { return true; } if (annotaion == null) { return true; } // 获取用户凭证token String token = request.getHeader("token"); // 判断token是否合法 if (!StringUtils.hasText(token)) { throw new UnauthenticatedException("请先登录"); } Claims claim = jwtUtils.getClaimByToken(token, "APP"); if (claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) { throw new UnauthenticatedException("请先登录"); } // 把用户信息存到session request.getSession().setAttribute(Const.USER_KEY, Long.parseLong(claim.getSubject())); return true; } }
spring: redis: host: localhost port: 6379
y用到了Redis
userId注入到会话session中,那么程序如何获取用户id呢,我在userService中写了这几个方法可以获取:
@Resource HttpSession httpSession; @Override public Long getCurrentUserId() { Long userId = (Long) httpSession.getAttribute(Const.USER_KEY); return userId == null ? -1 : userId; } @Override public AppUser getCurrentUser() { Long userId = getCurrentUserId(); return userId == null ? null : this.getById(userId); }