springboot基础搭建
版本说明
spring boot:2.6.7
jdk:1.8
springboot基础搭建
1. File->new->project
2. 选择“Spring Initializr”,jdk选择8最好,或者默认
3.补充项目信息
4.勾选常用依赖也可以后期再pom 里添加
5.包结构
6.配置数据源
(我是把application.properties 改成了application.yml 纯属个人习惯)
server:
port: 8080
spring:
datasource:
name: test
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://111.231.93.35:3306/ceshi?serverTimezone=CTT&useUnicode=true&characterEncoding=utf-8&useSSL=false
username: ljz
password: Ljz217645!
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimum-idle: 5
maximum-pool-size: 20
auto-commit: true
idle-timeout: 30000
pool-name: zedHikariCP
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
mybatis:
mapper-locations: classpath:mapper/*/*.xml
configuration:
map-underscore-to-camel-case: true
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
level:
root: info
com.zaxxer.hikari: warn
com.zed.datamanager.dao: debug #SQL显示
创建第一个页面,并跳转到相应页面
整合swagger
这里我们采用swager3.0.0
1.引入依赖
<properties>
<java.version>1.8</java.version>
<swagger.version>3.0.0</swagger.version>
</properties>
<!-- swager -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${swagger.version}</version>
</dependency>
2.添加配置文件
package com.zhonggg.web.swagger.config;
import io.swagger.models.auth.In;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import java.util.*;
/**
* 说明: swager 配置类
*
* @author ljz
* @version 1.0
* @date 2022/5/10 21:28
*/
@Configuration
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.zhonggg"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Api Documentation")
.contact(new Contact("", "", ""))
.version("1.0")
.build();
}
// 下面几个方法是为之后的token
/**
* 设置授权信息
*/
private List<SecurityScheme> securitySchemes() {
ApiKey apiKey = new ApiKey("token", "token", In.HEADER.toValue());
return Collections.singletonList(apiKey);
}
/**
* 授权信息全局应用
*/
private List<SecurityContext> securityContexts() {
return Collections.singletonList(
SecurityContext.builder()
.securityReferences(Collections.singletonList(new SecurityReference("token", new AuthorizationScope[]{new AuthorizationScope("global", "accessEverything")})))
.build()
);
}
@SafeVarargs
private final <T> Set<T> newHashSet(T... ts) {
if (ts.length > 0) {
return new LinkedHashSet<>(Arrays.asList(ts));
}
return null;
}
}
3.启动项目
这个原因是这是因为Springfox使用的路径匹配是基于AntPathMatcher的,而Spring Boot 2.6.X使用的是PathPatternMatcher。
解决方案在yml 文件中加入下面配置
spring:
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER
启动后访问页面:
http://localhost:8080/swagger-ui/index.html
整合shiro +JWT
引入依赖
<!--依赖版本-->
<properties>
<java.version>1.8</java.version>
<swagger.version>3.0.0</swagger.version>
<shiro.version>1.8.0</shiro.version>
<jwt.version>3.2.0</jwt.version>
</properties>
<!--整合Shiro安全框架-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<!--集成jwt实现token认证-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
jwt 工具类编写
实现功能:
- 生成token
- 校验token是否合法
- 通过载荷名字获取载荷的值
- 通过token获取用户
package com.zhonggg.web.security.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import java.io.UnsupportedEncodingException;
import java.util.Calendar;
import java.util.Date;
/**
* 说明: jwt 工具类用于token
*
* @author ljz
* @version 1.0
* @date 2022/5/13 14:43
*/
public class JwtUtils {
private static final String SECRET = "zhonggg";
/**
* 说明: 生成token
* @param: * @param username
* @param password
* @param minute
* @return: java.lang.String
* @author ljz
* @date: 2022/5/14 21:16
*/
public static String createToken(String username, String password,Integer minute ) throws UnsupportedEncodingException {
Calendar nowTime = Calendar.getInstance();
nowTime.add(Calendar.MINUTE, minute);
Date date = nowTime.getTime();
//密文生成
String token = JWT.create()
.withClaim("username", username)
.withClaim("password", password)
.withExpiresAt(date)
.withIssuedAt(new Date())
.sign(Algorithm.HMAC256(SECRET));
return token;
}
/**
* 说明: 校验token是否合法
* @param: * @param token
* @return: boolean
* @author ljz
* @date: 2022/5/14 21:17
*/
public static boolean verify(String token){
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
verifier.verify(token);
return true;
} catch (UnsupportedEncodingException e) {
return false;
}
}
/**
* 说明: 通过载荷名字获取载荷的值
* @param: * @param token
* @param name
* @return: java.lang.String
* @author ljz
* @date: 2022/5/14 21:18
*/
public static String getClaim(String token, String name){
String claim = null;
try {
claim = JWT.decode(token).getClaim(name).asString();
}catch (Exception e) {
return "getClaimFalse";
}
return claim;
}
public static String getUsername(String token){
try {
return getClaim(token,"username");
} catch (Exception e) {
return null;
}
}
}
封装token
封装token来替换Shiro原生Token,要实现AuthenticationToken接口
shiro默认supports的是UsernamePasswordToken,而我们现在采用了jwt的方式,所以这里我们自定义一个JwtToken,来完成shiro的supports方法。
package com.zhonggg.web.security.model;
import org.apache.shiro.authc.AuthenticationToken;
import org.springframework.stereotype.Component;
/**
* 说明: TODO
*
* @author ljz
* @version 1.0
* @date 2022/5/14 21:20
*/
@Component
public class JwtToken implements AuthenticationToken {
private String token ;
public JwtToken() {
}
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
添加JWT的过滤器
这个过滤器是我们的重点,这里我们继承的是Shiro内置的BasicHttpAuthenticationFilter,一个可以内置了可以自动登录方法的的过滤器。
这个过滤器是要注册到shiro配置里面去的,用来辅助shiro进行过滤处理。所有的请求都会到过滤器来进行处理。
我们需要重写几个方法:
- preHandle:拦截器的前置拦截,因为我们是前后端分析项目,项目中除了需要跨域全局配置之外,我们再拦截器中也需要提供跨域支持。这样,拦截器才不会在进入Controller之前就被限制了。
- isAccessAllowed:是否允许访问。如果带有 token,则对 token 进行检查,否则直接通过。如果请求头不存在Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
- isLoginAttempt:判断用户是否想要登入。检测 header 里面是否包含 Token 字段。
- executeLogin:executeLogin实际上就是先调用createToken来获取token,这里我们重写了这个方法,就不会自动去调用createToken来获取token,然后调用getSubject方法来获取当前用户再调用login方法来实现登录,这也解释了我们为什么要自定义jwtToken,因为我们不再使用Shiro默认的UsernamePasswordToken了。
package com.zhonggg.web.security.filter;
import com.zhonggg.web.security.model.JwtToken;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
/**
* 说明: 拦截
*
* @author ljz
* @version 1.0
* @date 2022/5/14 21:40
*/
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 说明: 拦截器的前置 最先执行的 这里只做了一个跨域设置
* @param: * @param request
* @param response
* @return: boolean
* @author ljz
* @date: 2022/5/14 21:42
*/
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
System.out.println("JwtFilter -----> preHandle() 方法执行");
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
res.setHeader("Access-control-Allow-Origin", req.getHeader("Origin"));
res.setHeader("Access-control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
res.setHeader("Access-control-Allow-Headers", req.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
res.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* preHandle 执行完之后会执行这个方法
* 再这个方法中 我们根据条件判断去去执行isLoginAttempt和executeLogin方法
* */
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
System.out.println("JwtFilter -----> isAccessAllowed() 方法执行");
/**
* 先去调用 isLoginAttempt方法 字面意思就是是否尝试登陆 如果为true
* 执行executeLogin方法
*/
if (isLoginAttempt(request,response)) {
executeLogin(request, response);
return true;
}
return true;
}
/**
* 这里我们只是简单去做一个判断请求头中的token信息是否为空
* 如果没有我们想要的请求头信息则直接返回false
* */
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
System.out.println( "JwtFilter -----> isLoginAttempt() 方法执行");
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("token");
return token != null;
}
/**
* 执行登陆
* 因为已经判断token不为空了,所以直接执行登陆逻辑
* 讲token放入JwtToken类中去
* 然后getSubject方法是调用到了MyRealm的 执行方法 因为上面我是抛错的所有最后做个异常捕获就好了
* */
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
System.out.println("JwtFilter -----> executeLogin() 方法执行");
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("token");
JwtToken jwtToken = new JwtToken(token);
//然后交给自定义的realm对象去登陆, 如果错误他会抛出异常并且捕获
System.out.println("-----执行登陆开始-----");
getSubject(request, response).login(jwtToken);
System.out.println("-----执行登陆结束----- 未抛出异常");
return true;
}
/**
* 将非法请求跳转到 /unauthorized/**
*/
private void responseError(ServletResponse response, String message) {
System.out.println("responseError");
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
//设置编码,否则中文字符在重定向时会变为空字符串
message = URLEncoder.encode(message, "UTF-8");
httpServletResponse.sendRedirect("/unauthorized/" + message);
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
自定义Realm
AccountRealm是shiro进行登录或者权限校验的逻辑所在,算是核心了,我们需要重写3个方法,分别是
- supports:为了让realm支持jwt的凭证校验
- doGetAuthorizationInfo:权限校验
- doGetAuthenticationInfo:登录认证校验
package com.zhonggg.web.security.model;
import cn.hutool.core.util.StrUtil;
import com.zhonggg.web.security.model.JwtToken;
import com.zhonggg.web.security.utils.JwtUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.stereotype.Component;
/**
* 说明: 自定义Realm
*
* @author ljz
* @version 1.0
* @date 2022/5/14 21:28
*/
@Component
public class MyRealm extends AuthorizingRealm {
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("MyRealm doGetAuthorizationInfo() 方法授权 ");
String token = principalCollection.toString();
String username = JwtUtils.getClaim(token,"username");
if (StrUtil.isBlank(username)) {
throw new AuthenticationException("token认证失败");
}
//TODO
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//其实这里应该是查询当前用户的角色或者权限去的,意思就是将当前用户设置一个svip和vip角色
//权限设置一级权限和耳机权限 正常来说应该是去读取数据库只添加当前用户的角色权限的
info.addRole("vip");
info.addRole("svip");
info.addStringPermission("一级权限");
info.addStringPermission("二级权限");
System.out.println("方法结束咯-------》》》");
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("认证-----------》》》》");
System.out.println("1.toString ------>>> " + authenticationToken.toString());
System.out.println("2.getCredentials ------>>> " + authenticationToken.getCredentials().toString());
System.out.println("3. -------------》》 " +authenticationToken.getPrincipal().toString());
String jwt = (String) authenticationToken.getCredentials();
String username= null;
//decode时候出错,可能是token的长度和规定好的不一样了
try {
username= JwtUtils.getUsername(jwt);
}catch (Exception e){
throw new AuthenticationException("token非法,不是规范的token,可能被篡改了,或者过期了");
}
if (!JwtUtils.verify(jwt)||username==null){
throw new AuthenticationException("token认证失效,token错误或者过期,重新登陆");
}
/*User user=userService.getUser(username);
if (user==null){
throw new AuthenticationException("该用户不存在");
}*/
return new SimpleAuthenticationInfo(jwt, jwt, "MyRealm");
}
}
添加shiro配置文件
配置文件实现:
- 创建defaultWebSecurityManagerBean对象
- 创建ShiroFilterFactoryBean来进行
- 过滤拦截,权限和登录
- 关闭session
- 添加注解权限开发
springBoot整合jwt与单纯的shiro实现认证有三个不一样的地方
- 因为不适用Session,所以为了防止会调用getSession()方法而产生错误,需要关闭session
- 关闭SHiroDao等
- 注册JwtFilter到ShiroFilterFactoryBea中
package com.zhonggg.web.security.config;
import com.zhonggg.web.security.model.MyRealm;
import com.zhonggg.web.security.filter.JwtFilter;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 说明: shiro 配置
*
* @author ljz
* @version 1.0
* @date 2022/5/14 21:51
*/
@Configuration
public class ShiroConfig {
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager(MyRealm myRealm){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
// 设置自定义 realm.
securityManager.setRealm(myRealm);
//关闭session
DefaultSubjectDAO subjectDAO=new DefaultSubjectDAO();
DefaultSessionStorageEvaluator sessionStorageEvaluator=new DefaultSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* 先走 filter ,然后 filter 如果检测到请求头存在 token,则用 token 去 login,走 Realm 去验证
*/
@Bean
public ShiroFilterFactoryBean factory(@Qualifier("securityManager")DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean factoryBean=new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap=new LinkedHashMap<>();
//设置我们自定义的JWT过滤器
filterMap.put("jwt",new JwtFilter());
factoryBean.setFilters(filterMap);
// 设置无权限时跳转的 url;
factoryBean.setUnauthorizedUrl("/unauthorized/无权限");
Map<String,String>filterRuleMap=new HashMap<>();
// 所有请求通过我们自己的JWT Filter
filterRuleMap.put("/**","jwt");
// 访问 /unauthorized/** 不通过JWTFilter
filterRuleMap.put("/unauthorized/**","anon");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
/**
* 添加注解支持,如果不加的话很有可能注解失效
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}