@
目录前言
为了巩固SpringBoot、Redis、Vue、Shiro框架整合的学习,拿这个小demo来练练手
一、Java后端接口开发
1. 新建SpringBoot 项目
1.1 开发技术栈:
- mysql、druid
- SpringBoot
- maven
- mybatis-Plus
- Shiro、JWT
- Redis
1.2 pom中jar包引入:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--mp-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<!--mp代码生成器、模板引擎-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.2.0</version>
</dependency>
<!--redis_shiro-->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis-spring-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.3</version>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
</dependency>
<!--log4j-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!--validation-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
1.3 配置文件:
spring:
datasource:
url: jdbc:mysql://localhost:3306/vueblog?useUnicode=true&characterEncoding=utf-8&userSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
initialsize: 5
minIdle: 1
maxActive: 20
maxWait: 60000
# 每隔多长时间进行空闲连接回收
timeBetweenEvictionRunMillis: 60000
# 每个连接的最小存活时间
minEvictableIdleTimeMillis: 300000
# 检验连接是否正常
validationQuery: SELECT 1 FROM DUAL
# 空闲时对连接进行检查
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 配置拦截统计的filters, 去掉后监控界面的sql无法统计
filters: stat,wall,log4j
2. 整合mybatis Plus
2.1 引入pom的jar包
<!--mp-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<!--mp代码生成器、模板引擎-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.2.0</version>
</dependency>
2.2 配置分页插件、代码生成器
2.3 配置文件
mybatis-plus:
mapper-locations: classpath*:/mapper/**Mapper.xml
configuration:
map-underscore-to-camel-case: true
3. Restful风格结果封装
封装一个Result类,包括Integer类型的code、String类型的msg、Object类型的data,封装success和fail方法。
4. 整合Shiro+JWT,并会话共享
考虑到之后可能会做集群、负载均衡等,所以就需要开启会话共享,而shiro的缓存和会话信息,我们一般使用redis来存储这些数据,所以,我们不仅仅需要整合shiro,同时也需要整合redis.
而因为我们做的是前后端分离项目的骨架,所以一般会采用token或者jwt作为跨域身份验证解决方案。所以整合shiro过程中,我们需要引入jwt的身份验证过程。
4.1 认证流程
4.2整合步骤
- ShiroConfig [Shiro主配置类,主要配置了安全管理器、实体数据源、缓存等]
//这里只展示了核心代码
//shiro权限数据和会话信息能够保存到redis中,实现会话共享
@Bean
public SessionManager sessionManager(RedisSessionDAO redisSessionDAO){
DefaultWebSessionManager sessionManager=new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO);
return sessionManager;
}
@Bean(name = "securityManager")
public SecurityManager securityManager(UserRealm userRealm,
SessionManager sessionManager,
RedisCacheManager redisCacheManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setCacheManager(redisCacheManager);
securityManager.setSessionManager(sessionManager);
securityManager.setRealm(userRealm);
//关闭shiro自带的session
DefaultSubjectDAO subjectDAO=new DefaultSubjectDAO();
DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
evaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(evaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
ShiroFilterChainDefinition shiroFilterChainDefinition){
ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String,Filter> filterMap=new HashMap<>();
filterMap.put("jwt",jwtFilter);
shiroFilterFactoryBean.setFilters(filterMap);
Map<String, String> filterChainMap = shiroFilterChainDefinition.getFilterChainMap();
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
return shiroFilterFactoryBean;
}
- UseRealm [用户实体数据源]
UserRealm继承于AuthorizingRealm类,重写了doGetAuthorizationInfo(用于授权)、doGetAuthenticationInfo(用于认证)、supports(用于支持jwt的凭证校验)
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
JWTToken token=(JWTToken)authenticationToken;
log.info("jwt-----------{}",token);
String userId =jwtUtils.getClaimByToken((String) token.getPrincipal()).getSubject();
User user = userService.getById(userId);
if(user==null)
throw new UnknownAccountException("用户不存在");
if(user.getStatus()==-1)
throw new LockedAccountException("账号被锁定");
ProfileUser profileUser = new ProfileUser();
BeanUtils.copyProperties(user,profileUser);
log.info("profile---------{}",profileUser.toString());
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(profileUser, token.getCredentials(), this.getName());
return simpleAuthenticationInfo;
}
- JWTToken [自定义Token]
自定义token继承于AuthenticationToken,用于包装jwt
/**
* 自定义JWTToken
*/
public class JWTToken implements AuthenticationToken {
private String token;
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
public JWTToken(String token){
this.token=token;
}
}
- JWTFilter [自定义拦截器,拦截所有请求进行jwt判断校验]
继承于AuthenticatingFilter(或者是BasicHttpAuthenticationFilter),本次采用AuthenticatingFilter
//自定义token,然后交给shiro验证
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt=request.getHeader("Authorization");
if(StringUtils.isEmpty(jwt))
return null;
else
return new JWTToken(jwt);
}
//拦截校验
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
//请求头没有jwt,身份为游客,直接放行
if(StringUtils.isEmpty(jwt))
return true;
//否则用户需要进行登录验证操作
else{
//验证jwt有效
Claims claim = jwtUtils.getClaimByToken(jwt);
if(claim==null || jwtUtils.isJwtExpire(claim.getExpiration()))
throw new ExpiredCredentialsException("token已经过期,请重新登录");
}
//shiro登录验证
return executeLogin(servletRequest,servletResponse);
}
当JWTFilter拦截处理后,拥有jwt并且有效的用户会执行executeLogin方法进行登录,实质还是交给UserRealm进行处理,此时UserRealm中存储的就是封装的JWTToken数据。
- ProfileUser [UserRealm登录认证通过后保存在Subject中的数据]
@Data
public class ProfileUser implements Serializable {
private Integer id;
private String username;
private String avatar;
}
5. 异常处理
- 程序中需要处理 一般的运行时异常、shiro处理异常、前端数据实体校验异常、数据处理结果Assert断言异常
- 全局异常处理由@RestControllerAdvice指明,在每种异常处理前使用@ExceptionHandler指明异常类型,@ResponseStatus指定返回的状态码。
//捕捉shiro异常
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(ShiroException.class)
public Result shiroHandler(ShiroException e){
log.error("shiro认证异常---------");
return Result.fail(401,e.getMessage());
}
//捕捉实体校验异常
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result methodHandler(MethodArgumentNotValidException e){
log.error("实体数据异常--------");
BindingResult bindingResult = e.getBindingResult();
ObjectError error = bindingResult.getAllErrors().stream().findFirst().get();
return Result.fail(error.getDefaultMessage());
}
6. 实体校验
- Java后端实体校验常用技术:JSR303、JSR-349、hibernate validation、spring validation等。其中JSR303是一种规范,JSR-349是它的升级版本,它们规范了一些校验注解,比如@Null、@NotNull、@Pattern,它们位于javax.validator.constraints包下,只提供规范但是不提供实现。
- hibernate validation则提供相应的实现,并且提供了一些新的注解,比如@Email、@Length、@Range、@Size等,它们位于org.hibernate.validator.constraints包下。
- spring validation对hibernate validation进行了二次封装,在springmvc模块中添加了自动校验,加快了web开发速度。
校验技术的使用:
由于引入的spring-boot-starter-web依赖的子依赖中包含了hibernate-validator、jackson-databind依赖,不需要额外引入
//校验示例
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 用户名
*/
@NotBlank(message = "用户名不能为空")
private String username;
/**
* 用户头像
*/
private String avatar;
/**
* 用户邮箱
*/
@Email(message = "邮箱不符合规范")
private String email;
/**
* 登录密码
*/
@Pattern(regexp = "^[a-z A-Z 0-9]{6,}",message = "密码必须为字母或者数字的6位组合")
private String password;
}
//SpringMVC校验写法如下,bindingResult 是结果集,可能会包含异常
@Controller
public class FooController {
@RequestMapping("/foo")
public String foo(@Validated Foo foo <1>, BindingResult bindingResult <2>) {
if(bindingResult.hasErrors()){
for (FieldError fieldError : bindingResult.getFieldErrors()) {
//...
}
return "fail";
}
return "success";
}
}
校验注解总结:
JSR提供的校验注解:
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max=, min=) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
Hibernate Validator提供的校验注解:
@NotBlank(message =) 验证字符串非null,且长度必须大于0
@Email 被注释的元素必须是电子邮箱地址
@Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range(min=,max=,message=) 被注释的元素必须在合适的范围内
7. 跨域问题
什么是跨域?
跨域就是指浏览器不能执行其它网站的脚本,它是由于浏览的同源策略造成的,是浏览器对JavaScript实施的安全限制。
首先狭义的同源就是指:域名、协议、端口均相同:
同源策略限制了Cookie、LocalStorage无法读取;DOM和JS对象无法获取;Ajax请求发送不出去。
http://www.yyy.cn/index.html 调用 http://www.xxxyyy.cn/server.php 非跨域
http://**www.xxxyyy.cn**/index.html 调用 http://**www.xxx.cn**/server.php 跨域,主域不同
http://**abc**.xxxyyy.cn/index.html 调用 http://**def**.xxx.cn/server.php 跨域,子域名不同
http://www.xxx.cn:**8080**/index.html 调用 http://www.xxx.cn/server.php 跨域,端口不同
**https**://www.xxx.cn/index.html 调用 **http**://www.xxx.cn/server.php 跨域,协议不同
如何解决跨域问题?
-
跨域资源共享CORS 这是目前的主流方案,全称是(Cross-origin resource sharing),它允许浏览器向跨源服务器发送请求,克服了Ajax只能同源使用的限制。
-
整个CORS过程不需要用户的参与,都是浏览器自动完成。浏览器一旦发现Ajax请求跨域,就会自动添加一些头信息,甚至是进行一次附加请求。因此实现CORS的关键是服务器,对服务器动手脚使得浏览器能够跨域。
-
分为两种请求:简单请求和复杂请求
(1) 简单请求 [请求方式为HEAD、POST或者GET]
浏览器发送CORS请求,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。Origin字段表明此次请求来源于哪个源(协议+域名+端口)。
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0
服务器根据这个值确定是否同意这次请求,如果Origin不在许可范围内,服务器会返回一个正常的HTTP回应,状态码可能为200。浏览器此时发现返回的响应头中不包含Access-Control-Allow-Origin
,就抛出一个错误被XMLHttpRequest回调函数捕获。
如果Origin在许可范围内,就会返回如下字段:
#该字段必须,表示请求的来源
Access-Control-Allow-Origin: http://api.bob.com
#可选,表示是否允许发送Cookie,如果要发送,还必须在AJAX请求中打开withCredential属性
Access-Control-Allow-Credentials: true
#可选,用于拿到其它额外的字段
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
(2)非简单请求 [请求方式为PUT、DELETE或者Content-Type是applicaiton/json]
非简单会在发送请求前进行一次预检请求,用于询问服务器当前域名是否在许可名单之中,以及可以使用哪些HTTP动词和头信息字段。
预检请求:
OPTIONS /cors HTTP/1.1 #请求方式为OPTIONS
Origin: http://api.bob.com #指定请求来源
Access-Control-Request-Method: PUT #必须字段,用于指出CORS会用到哪些HTTP方法
Access-Control-Request-Headers: X-Custom-Header #必须,用于指出CORS会额外发送的头信息字段,上例为x-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
预检响应:
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT #必需字段,服务器支持的所有请求方式
Access-Control-Allow-Headers: X-Custom-Header #必须字段,服务器支持所有头信息字段
Content-Type: text/html; charset=utf-8
当服务器通过了预检请求,接下来每次浏览器的CORS请求都和简单请求一样,会包含Origin头信息字段。服务器的回应也会包含Access-Control-Allow-Origin
字段。
好像扯远了。。。。回到正题!
正题:
在JWTFilter中需要重写preHandle方法来操纵服务器通过浏览器的跨域请求。
//对跨域提供支持
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest= WebUtils.toHttp(request);
HttpServletResponse httpServletResponse=WebUtils.toHttp(response);
httpServletResponse.setHeader("Access-control-Allow-Origin",httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-control-Allow-Methods","GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-control-Allow-Headers",httpServletRequest.getHeader("Access-Control-Request-Headers"));
//复杂请求跨域时首先会发送一个预检请求(OPTIONS),这里直接返回正常状态
if(httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())){
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request,response);
}
另外还需要配置一下全局跨域的处理:
/**
* 解决跨域问题
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET","POST","PUT","HEAD","OPTIONS","DELETE")
.allowCredentials(true)
.allowedHeaders("*");
}
}
8. 登录接口开发
登录的逻辑:用户登录就是一个刷新jwt的过程,接收用户的用户名和密码然后生成相应的jwt,再将该jwt放在header上返回给前端。
@PostMapping("/login")
public Result login(@Validated @RequestBody LoginDto loginDto, HttpServletResponse response){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username",loginDto.getUsername());
//判断用户名和密码
User one = userService.getOne(wrapper);
Assert.notNull(one,"用户不存在!");
if(!one.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))){
return Result.fail("密码错误!");
}
//生成jwt放入响应头,以后每次请求都会携带jwt
String jwt = jwtUtils.generateToken(one.getId().toString());
response.setHeader("Authorization",jwt);
//在涉及跨域请求时,response中大部分header需要源服务端同意才能拿到,所以需要在response中增加一个如下header
response.setHeader("Access-Control-Expose-Headers","Authorization");
return Result.success("登录成功", MapUtil.builder()
.put("id",one.getId())
.put("username",one.getUsername())
.put("avatar",one.getAvatar())
.put("email",one.getEmail())
.put("id",one.getId())
.put("id",one.getId()));
}
9. 博客接口开发
简单的增删改查,如果需要用户权限,在后端的接受方法上加上@RequirePermissions、@RequireRoles、@RequireAuthentication等注解。
具体看源码哈~
参考文章,感谢以下所有作者:
项目原作者:MarkerHub
项目视频地址: https://www.bilibili.com/video/BV1PQ4y1P7hZ
项目文档地址:https://juejin.im/post/6844903823966732302
学习过程:
shiro整合Springboot : https://www.jianshu.com/p/ef0a82d471d2
SpringBoot整合Jwt: https://www.jianshu.com/p/3c51832f1051
JWT构造详解:https://www.jianshu.com/p/1ebfc1d78928
什么是跨域,如何实现: https://www.jianshu.com/p/f049ac7e2220
https://blog.csdn.net/swl1993831/article/details/90905040
redis学习: https://blog.csdn.net/xgangzai/article/details/82661940
@Validate注解: https://blog.csdn.net/u013815546/article/details/77248003/
java8 stream(): https://blog.csdn.net/m0_37556444/article/details/84975355
@Bean注解: https://www.cnblogs.com/cxuanBlog/p/11179439.html
shiro报错 UnavailableSecurityManagerException :https://www.cnblogs.com/ginponson/p/6217057.html
总结
顺手给个star吧 ❤
代码地址: https://github.com/iStitches/vueBlog
好好学习,坚持下去!