4.4.6 SpringBoot 解决跨域问题 CorsConfig
访问地址 http://localhost:7000/login
输入用户名、密码、验证码后,访问后端地址 http://localhost:9090/login
,跨域访问
报错:
上一节给出解决方案:Controller
类加个注解 @CrossOrigin
,但每次新增 Controller
类都需要手工添加注解,比较麻烦。SpringBoot
提供过滤器 CorsFilter
统一处理 跨域访问
问题。
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;@Configuration
public class CorsConfig {// 当前跨域请求最大有效时长。这里默认1天private static final long MAX_AGE = 24 * 60 * 60;@Beanpublic CorsFilter corsFilter() {UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();CorsConfiguration corsConfiguration = new CorsConfiguration();corsConfiguration.addAllowedOrigin("*"); // 1 设置访问源地址corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法corsConfiguration.setMaxAge(MAX_AGE);source.registerCorsConfiguration("/**", corsConfiguration); // 4 对接口配置跨域设置return new CorsFilter(source);}
}
4.4.7 后端接口
WebController
@RestController
public class WebController {@ResourceUserService userService;@PostMapping("/login")public Result login(@RequestBody User user) {System.out.println(user);if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {return Result.error("数据输入不合法");}user = userService.login(user);return Result.success(user);}@PostMapping("/register")public Result register(@RequestBody User user) {if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {return Result.error("数据输入不合法");}if (user.getUsername().length() > 10 || user.getPassword().length() > 20) {return Result.error("数据输入不合法");}user = userService.register(user);return Result.success(user);}
}
UserServiceImpl
@Override
public User login(User user) {// 根据用户名查询数据库的用户信息User dbUser = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, user.getUsername()));if (dbUser == null) {// 抛出一个自定义的异常throw new ServiceException("用户名或密码错误");}if (!user.getPassword().equals(dbUser.getPassword())) {throw new ServiceException("用户名或密码错误");}// 生成tokenString token = TokenUtils.createToken(dbUser.getId(), dbUser.getPassword());dbUser.setToken(token);return dbUser;
}@Override
public User register(User user) {User dbUser = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, user.getUsername()));if (dbUser != null) {// 抛出一个自定义的异常throw new ServiceException("用户名已存在");}user.setName(user.getUsername());userMapper.insert(user);return user;
}
自定义异常 ServiceException
@Getter
public class ServiceException extends RuntimeException {private final String code;public ServiceException(String msg) {super(msg);this.code = "500";}public ServiceException(String code, String msg) {super(msg);this.code = code;}}
GlobalException
@ControllerAdvice
public class GlobalException {@ExceptionHandler(ServiceException.class)@ResponseBodypublic Result serviceException(ServiceException e) {return Result.error(e.getCode(), e.getMessage());}}
4.5 SpringBoot集成JWT token实现权限验证
4.5.1 pom.xml添加JWT依赖
<!-- JWT -->
<dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>4.3.0</version>
</dependency>
4.5.2 工具类 TokenUtils
@Component
public class TokenUtils {private static UserMapper staticUserMapper;@ResourceUserMapper userMapper;@PostConstructpublic void setUserService() {staticUserMapper = userMapper;}/*** 生成token** @return*/public static String createToken(String userId, String sign) {return JWT.create().withAudience(userId) // 将 user id 保存到 token 里面,作为载荷.withExpiresAt(DateUtil.offsetHour(new Date(), 2)) // 2小时后token过期.sign(Algorithm.HMAC256(sign)); // 以 password 作为 token 的密钥}/*** 获取当前登录的用户信息** @return user对象*/public static User getCurrentUser() {try {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String token = request.getHeader("token");if (StrUtil.isNotBlank(token)) {String userId = JWT.decode(token).getAudience().get(0);return staticUserMapper.selectById(Integer.valueOf(userId));}} catch (Exception e) {return null;}return null;}
}
4.5.3 login() 方法增加 token 返回
@Override
public User login(User user) {// 根据用户名查询数据库的用户信息User dbUser = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, user.getUsername()));if (dbUser == null) {// 抛出一个自定义的异常throw new ServiceException("用户名或密码错误");}if (!user.getPassword().equals(dbUser.getPassword())) {throw new ServiceException("用户名或密码错误");}// 生成tokenString token = TokenUtils.createToken(String.valueOf(dbUser.getId()), dbUser.getPassword());dbUser.setToken(token);return dbUser;
}
Login.vue 将返回 token
存储本地
login() {
this.$refs["loginRef"].validate((valid) => {if (valid) {// 验证通过this.$request.post("/login", this.user).then((res) => {if (res.code === "200") {// 登录成功,跳转到首页this.$router.push("/");this.$message.success("登录成功");localStorage.setItem("honey-user", JSON.stringify(res.data.token)); // 存储 token 到本地} else {this.$message.error(res.msg);}});}
});
登录成功后,本地存储数据:
📌前端接口在每次请求后端数据的时候,都会在请求头带上这个 token
作为验证信息。
📅 request.js:
请求拦截器:对请求头增加 token
request.interceptors.request.use(config => {config.headers['Content-Type'] = 'application/json;charset=utf-8';// 设置请求头,增加tokenlet token = JSON.parse(localStorage.getItem("honey-user") || '{}')config.headers['token'] = tokenreturn config;
}
响应拦截器:判断权限不足,重定向登录页面
request.interceptors.response.use(response => {let res = response.data;// 兼容服务端返回的字符串数据if (typeof res === 'string') {res = res ? JSON.parse(res) : res}// 拦截权限不足的请求,重定向登录页面,防止直接输入网址访问if (res.code === '401') {router.push('/login')}return res;}
)
如果不登录直接访问 http://localhost:7000/
,后台接口返回错误码 401
,会被响应拦截器拦截,重定向到登录页面。登录完成后,本地存储 token
,后续访问后端请求从本地存储获取到 token
,才能正常访问。
4.5.4 自定义注解 AuthAccess
import java.lang.annotation.*;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthAccess {
}
该注解用于标注权限放行的方法。
@AuthAccess
@PostMapping("/register")
public Result register(@RequestBody User user) {if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {return Result.error("数据输入不合法");}if (user.getUsername().length() > 10 || user.getPassword().length() > 20) {return Result.error("数据输入不合法");}user = userService.register(user);return Result.success(user);
}
WebController 的方法 register()
标注注解 @AuthAccess
,结合下面拦截器 JwtInterceptor
对该注解的处理,register()
方法将被放行。
4.5.5 自定义拦截器 JwtInterceptor
public class JwtInterceptor implements HandlerInterceptor {@Resourceprivate UserMapper userMapper;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {String token = request.getHeader("token");if (StringUtils.isBlank(token)) {token = request.getParameter("token");}// 对标注 AuthAccess 注解的方法进行放行if (handler instanceof HandlerMethod) {AuthAccess annotation = ((HandlerMethod) handler).getMethodAnnotation(AuthAccess.class);if (annotation != null) {return true;}}// 判断前端上送 token,执行认证if (StringUtils.isBlank(token)) {throw new ServiceException("401", "请登录");}// 获取 token 中的 user idString userId;try {userId = JWT.decode(token).getAudience().get(0);} catch (JWTDecodeException j) {throw new ServiceException("401", "请登录");}// 根据token中的userid查询数据库User user = userMapper.selectById(userId);if (user == null) {throw new ServiceException("401", "请登录");}// 用户密码加签验证 tokenJWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();try {jwtVerifier.verify(token); // 验证token} catch (JWTVerificationException e) {throw new ServiceException("401", "请登录");}return true;}
}
📌 请求头获取 token
字段值,进行JWT 认证判断是否为登录成功生成的 token
,进而进行权限认证。
💦
if (handler instanceof HandlerMethod)
的含义是什么?
1.springmvc 启动时候,扫描所有 controller 类,解析所有映射方法,将每个映射方法封装一个对象
HandlerMethod
,该类包含所有请求映射方法信息(映射路径 / 方法名 / 参数 / 注解 / 返回值),上例中AuthAccess annotation = ((HandlerMethod) handler).getMethodAnnotation(AuthAccess.class)
,就是获取请求方法是否标注AuthAccess
注解。
2.springmvc 针对这些请求映射方法信息封装对象类,使用类似 map 的数据结构进行统一管理Map<String, HandlerMethod> map
3.页面发起请求时(/users/currentUser),进入拦截器之后,springmvc 自动解析请求路径,得到 url(/users/currentUser),获取url之后,进而获取 /users/currentUser 路径对应的映射方法HandlerMethod
实例
4.调用拦截器preHandle
方法并将请求对象、响应对象、映射方法对象handler
一起传入。
📖 登录拦截器原理
1.在Spring MVC中,拦截器的
preHandle
、postHandle
、afterCompletion
方法的第三个参数是一个 Object 类型的handler
参数。这个handler
参数实际上就是处理当前请求的处理器。
2.在Spring MVC中,处理器不一定是HandlerMethod
类型的。例如,当请求的URL对应的是一个静态资源时,处理器可能是ResourceHttpRequestHandler
类型的。
3.因此,如果你的拦截器的代码只适用于HandlerMethod
类型的处理器,你需要在代码中加入if (handler instanceof HandlerMethod)
这样的判断,以确保代码不会在处理其他类型的处理器时出错。
4.在Spring MVC中,HandlerMethod
是一个特殊的处理器类型,它用于处理由@RequestMapping
注解(或其变体,如@GetMapping、@PostMapping等)标注的方法。
📖 Springmvc拦截器的时候要加判断 handler instanceof HandlerMethod
4.5.6 配置拦截器 InterceptorConfig
@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {@Overrideprotected void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(jwtInterceptor()).addPathPatterns("/**") // 1. 设置拦截路径.excludePathPatterns("/login"); // 2. 设置放行路径super.addInterceptors(registry);}@Beanpublic JwtInterceptor jwtInterceptor() {return new JwtInterceptor();}
}
.addPathPatterns("/**")
:对所用请求地址进行拦截
.excludePathPatterns("/login")
:设置放行路径,此处对 /login
进行放行,不进行拦截处理,即不校验 token
。如果想对整个路径放行,可以设置 /login/**
,即对 /login
下所有路径放行。
.excludePathPatterns(url)
和 注解@AuthAccess
结合使用,可以灵活设置放行方法。
4.6 单文件、多文件上传和下载
4.6.1 文件上传、下载 Java 代码
@RestController
@RequestMapping("/file")
public class FileController {@Value("${ip:localhost}")String ip;@Value("${server.port}")String port;private static final String ROOT_PATH = System.getProperty("user.dir") + File.separator + "files";@PostMapping("/upload")public Result upload(MultipartFile file) throws IOException {// 文件的原始名称String originalFilename = file.getOriginalFilename();// 获取文件名称、后缀名String mainName = FileUtil.mainName(originalFilename);String extName = FileUtil.extName(originalFilename);// 如果当前文件的父级目录不存在,就创建if (!FileUtil.exist(ROOT_PATH)) {FileUtil.mkdir(ROOT_PATH);}// 如果当前上传的文件已经存在了,那么重命名一个文件if (FileUtil.exist(ROOT_PATH + File.separator + originalFilename)) {originalFilename = System.currentTimeMillis() + "_" + mainName + "." + extName;}File saveFile = new File(ROOT_PATH + File.separator + originalFilename);// 存储文件到本地的磁盘里面去file.transferTo(saveFile);String url = "http://" + ip + ":" + port + "/file/download/" + originalFilename;// 返回文件的链接,这个链接就是文件的下载地址,这个下载地址就是我的后台提供出来的return Result.success(url);}@AuthAccess@GetMapping("/download/{fileName}")public void download(@PathVariable String fileName, HttpServletResponse response) throws IOException {// 附件下载response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));// 预览// response.addHeader("Content-Disposition", "inline;filename=" + URLEncoder.encode(fileName, "UTF-8"));String filePath = ROOT_PATH + File.separator + fileName;if (!FileUtil.exist(filePath)) {return;}byte[] bytes = FileUtil.readBytes(filePath);ServletOutputStream outputStream = response.getOutputStream();outputStream.write(bytes);outputStream.flush();outputStream.close();}
}
📅 响应头 Content-Disposition
为 attachment;filename=
,文件以附件形式下载;
📅Content-Disposition
为 inline;filename=
,图片和 pdf 可以预览,其他文件类型还是以附件形式下载。