前后端分离后端api环境搭建
需要用到一下技术栈:
-
SpringBoot
-
Shiro
-
Jwt
-
MyBatisPlus
-
Swagger
-
Redis
-
Googlekaptcha (谷歌的验证码插件)
Git:https://gitee.com/jydm520/springbootApi.git
第一步:导入依赖
在初始化好的springboot项目中添加下面的依赖
<!-- mysql驱动程序--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> ? <!-- mybatisplus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.2</version> </dependency> <!-- mybatisplus代码生成器--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.4.1</version> </dependency> <!--生成器模板引擎--> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.28</version> </dependency> ? <!-- swagger2--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> <!-- ui 插件 --> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>swagger-bootstrap-ui</artifactId> <version>1.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <!--jwt--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.8.2</version> </dependency> <!-- jedis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.5.2</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.9.0</version> </dependency> <!-- springboot shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.1</version> </dependency> <!-- jwt --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!-- 验证码图片工具类--> <dependency> <groupId>com.github.axet</groupId> <artifactId>kaptcha</artifactId> <version>0.0.9</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.5</version> </dependency> ? </dependencies>
第二步:整合MyBatisPlus
因为上面都已经把依赖导入了下面的所有配置就省略了导入以来的步骤
编写配置文件
@Configuration @MapperScan("com.jydm.api.Mapper") public class MybatisPlusConfig { // 最新版 @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } @Bean public MybatisPlusInterceptor OptimisticLocker() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } ? }
配置据库
Application.yml
spring: datasource: username: root password: niuniu url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver
注意以下最新版的mysql驱动需要设置时区不然会时区报错
配置MyBatisPlus日志
Application.yml
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
这里就可以测试以下MyBatisPlus是不是可以用了
整合代码自动生成工具(可选)
public class AutoCode { /** * <p> * 读取控制台内容 * </p> */ public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + ":"); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotBlank(ipt)) { return ipt; } } throw new MybatisPlusException("请输入正确的" + tip + "!"); } ? public static void main(String[] args) { // 代码生成器 AutoGenerator mpg = new AutoGenerator(); ? // 全局配置 GlobalConfig gc = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); gc.setOutputDir(projectPath + "/src/main/java"); gc.setAuthor("jydm"); gc.setOpen(false); gc.setFileOverride(false);//是否覆盖 gc.setServiceName("%sService");//去除Service I前缀 gc.setDateType(DateType.ONLY_DATE);//日期类型 gc.setSwagger2(true); //实体属性 Swagger2 注解 mpg.setGlobalConfig(gc); ? // 数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/shop?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai"); dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword("niuniu"); dsc.setDbType(DbType.MYSQL);//设置数据库类型 mpg.setDataSource(dsc); ? // 包配置 PackageConfig pc = new PackageConfig(); pc.setModuleName("api");//模块名字 pc.setParent("com.jydm");//包路径 pc.setEntity("pojo");//实体类包名 pc.setMapper("mapper"); //mapper包名 pc.setService("service"); //service包名 pc.setController("controller");//controller包名 mpg.setPackageInfo(pc); ? // 自定义配置 InjectionConfig cfg = new InjectionConfig() { @Override public void initMap() { // to do nothing } }; // 如果模板引擎是 freemarker String templatePath = "/templates/mapper.xml.ftl"; // 自定义输出配置 List<FileOutConfig> focList = new ArrayList<>(); // 自定义配置会被优先输出 focList.add(new FileOutConfig(templatePath) { @Override public String outputFile(TableInfo tableInfo) { // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!! return projectPath + "/src/main/resources/mapper/" + pc.getModuleName() + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; } }); ? cfg.setFileOutConfigList(focList); mpg.setCfg(cfg); ? // 策略配置 StrategyConfig strategy = new StrategyConfig(); strategy.setInclude(scanner("表名")); strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setEntityLombokModel(true);//自动lombok strategy.setRestControllerStyle(true);//controller restful风格 strategy.setLogicDeleteFieldName("deleted");//逻辑删除 strategy.setControllerMappingHyphenStyle(true); // localhost:8080/hello_id_2 strategy.setVersionFieldName("version");// 乐观锁 //自动填充配置 TableFill gmtCreate = new TableFill("create_time", FieldFill.INSERT);//创建时间填充 TableFill gmtModified = new TableFill("update_time", FieldFill.INSERT_UPDATE);//插入更细时间填充 ArrayList<TableFill> tableFills = new ArrayList<>(); tableFills.add(gmtCreate); tableFills.add(gmtModified); strategy.setTableFillList(tableFills); mpg.setStrategy(strategy); ? mpg.setTemplateEngine(new FreemarkerTemplateEngine()); //启动 mpg.execute(); } }
第三步:整合Swagger
编写配置文件
@Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket docket(){ return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors //controller包路径 .basePackage("com.jydm.api.controller")) //过滤 //.paths(PathSelectors.ant("/")) .build(); //是否开启 //.enable(false); } //界面信息 访问网址/doc.html public ApiInfo apiInfo(){ return new ApiInfo("简易代码Swagger", "自研自主学习平台", "1.0", "urn:tos", new Contact("简易代码", "www.baidu.com", "1656641922@qq.com"), "Apache 2.0", "http://www.apache.org/licenses/LICENSE-2.0", new ArrayList()); }
swagger还是很好配置的只需要配置这个配置文件
还可以定义识别发布版本关闭swagger这个想弄的网上一查都有这里就不介绍了不是重点
因为我用了ui插件所以访问Swagger的路径为doc.html(个人喜欢比较好看)
第四步:Redis
编写配置文件(直接cv)
@EnableCaching @Configuration public class RedisConfig extends CachingConfigurerSupport { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); RedisSerializer<String> redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); template.setConnectionFactory(factory); //key序列化方式 template.setKeySerializer(redisSerializer); //value序列化 template.setValueSerializer(jackson2JsonRedisSerializer); //value hashmap序列化 template.setHashValueSerializer(jackson2JsonRedisSerializer); return template; } ? @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisSerializer<String> redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); //解决查询缓存转换异常的问题 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); // 配置序列化(解决乱码的问题),过期时间600秒 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(600)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) .disableCachingNullValues(); RedisCacheManager cacheManager = RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); return cacheManager; } }
application.yml
spring:
redis:
# 服务器地址
host: 192.168.19.100
# 端口号
port: 6379
# 数据库索引
database: 0
# 链接超时时间
timeout: 1800000
lettuce:
pool:
# 连接池最大链接数
max-active: 200
# 最大阻塞等待时间(负数表示没限制)
max-wait: -1
# 连接池中最大空闲链接
max-idle: 5
# 连接池中最小空闲连接
min-idle: 0
这里还可以写一个Redis的工具类 这个太长了直接上文件
第五步:统一结果封装
/**
* 统一结果封装类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Integer code;
private String msg;
private Object data=null;
public static Result succ(Object data) {
return succ(200, "操作成功", data);
}
public static Result succ(int code, String msg, Object data)
{
Result r = new Result();
r.setCode(code);
r.setMsg(msg);
r.setData(data);
return r;
}
public static Result fail(String msg) {
return fail(400, msg, null);
}
public static Result fail(String msg, Object data) {
return fail(400, msg, data);
}
public static Result fail(int code, String msg, Object data) {
Result r = new Result();
r.setCode(code);
r.setMsg(msg);
r.setData(data);
return r;
}
}
第六步:验证码
编写配置文件
@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; } }
这里就是验证码图片的一些属性
验证码控制器
public class KaptchController { @Autowired private RedisTemplate redisTemplate; @Autowired private Producer producer; @GetMapping ("/api/captcha") public Result getKaptch(HttpServletRequest request){ //每次获取前删除原有验证码 String token = request.getParameter("codeKey"); if (token!=null){ redisUtils.hDelete(Const.captcha_KEY,token); } //生成验证码的key String key= UUID.randomUUID().toString(); //生成验证码 String code=producer.createText(); //生成图片 BufferedImage image=producer.createImage(code); ByteArrayOutputStream op=new ByteArrayOutputStream(); try { ImageIO.write(image,"jpg",op); } catch (IOException e) { e.printStackTrace(); } //将图片转为base64格式 BASE64Encoder encoder = new BASE64Encoder(); String str = "data:image/jpeg;base64,"; String base64Img = str + encoder.encode(op.toByteArray()); redisTemplate.boundHashOps(Const.captcha_KEY).put(key,code); redisTemplate.boundHashOps(Const.captcha_KEY).expire(1, TimeUnit.MINUTES); log.info("验证码 -- {} - {}", key, code); HashMap<Object, Object> map = new HashMap<>(); map.put("codekey", key); map.put("base64Img", base64Img); return Result.succ(map); } }
验证码hashkey的一个常量
public class Const { //redis hash验证码的key public final static String captcha_KEY="captcha"; }
第七步:Shiro+Jwt
最繁琐的地方,也是前后端分离身份认证最关键的地方
配置Jwt相关:
自定义Token
重写了shiro的HostAuthenticationToken,RememberMeAuthenticationToken类
@Data public class JwtToken implements HostAuthenticationToken, RememberMeAuthenticationToken { @Autowired JwtUtils jtUtils; private String token; private char[] password; private boolean rememberMe; private String host; public JwtToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return this.password; } public JwtToken(String token, String password) { this(token, (char[])(password != null ? password.toCharArray() : null), false, (String)null); } public JwtToken(String token, char[] password, boolean rememberMe, String host) { this.rememberMe = false; this.token = token; this.password = password; this.rememberMe = rememberMe; this.host = host; } }
JwtUtils
这里主要是提供一些对jwt的操作
@Data @Component @ConfigurationProperties(prefix = "jydm.jwt")//绑定到配置文件 可以在yml给属性初始化值 public class JwtUtils { private long expire; private String secret; private String header; // 生成jwt public String generateToken(String username) { Date nowDate = new Date(); Date expireDate = new Date(nowDate.getTime() + 1000 * expire); return Jwts.builder() .setHeaderParam("typ", "JWT") .setId(username) .setSubject(username) .setIssuedAt(nowDate) .setExpiration(expireDate)// 7天過期 .signWith(SignatureAlgorithm.HS512, secret) .compact(); } // 解析jwt public Claims getClaimByToken(String jwt) { try { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(jwt) .getBody(); } catch (Exception e) { return null; } } // jwt是否过期 public boolean isTokenExpired(Claims claims) { return claims.getExpiration().before(new Date()); } }
JwtFilter
@Slf4j public class JwtFilter extends AuthenticatingFilter { @Resource JwtUtils jwtUtils; @Override protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { HttpServletRequest request = (HttpServletRequest) servletRequest; String jwt = request.getHeader("Authorization"); if(StringUtils.isEmpty(jwt)||"null".equals(jwt)) { return null; } return new JwtToken(jwt); } ? @Override protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { HttpServletRequest request = (HttpServletRequest) servletRequest; String jwt = request.getHeader("Authorization"); if("null".equals(jwt)||jwt==null||StringUtils.isEmpty(jwt)){ return true; } else { // 校验jwt Claims claim = jwtUtils.getClaimByToken(jwt); if(claim == null || jwtUtils.isTokenExpired(claim)){ throw new ExpiredCredentialsException("token已失效,请重新登录");//抛出一个异常 进行统一异常处理 } // 执行登录 return executeLogin(servletRequest, servletResponse); } } //登录失败 @Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { HttpServletResponse httpServletResponse = (HttpServletResponse) response; Throwable throwable = e.getCause() == null ? e : e.getCause(); String json = e.getMessage(); try { httpServletResponse.getWriter().print(json); } catch (IOException ioException) { } return false; } @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请求,这里我们给OPTIONS请求直接返回正常状态 if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value()); return false; } return super.preHandle(request, response); } }
Realm
@Slf4j @Component public class UserRealm extends AuthorizingRealm { @Autowired UserService userService; @Autowired JwtUtils jwtUtils; //开启上面自定义的token @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } //授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); Subject subject = SecurityUtils.getSubject(); //获取真证对象 String principal =(String) subject.getPrincipal(); User user = userService.getUser(principal); log.info("user",user); info.addRole(user.getAuthor()); return info; } //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { JwtToken usertoken=(JwtToken) token; String jwt = usertoken.getToken(); Claims claim = jwtUtils.getClaimByToken(jwt); User login = userService.getUser(claim.getId()); if (login==null){ return null; //自动抛出UnknownAccountException异常 }else { return new SimpleAuthenticationInfo(login.getUsername(),login.getPassword(),getName()); } ? } }
ShiroConfig
@Configuration public class ShiroConfig { @Autowired private UserRealm userRealm; @Autowired private MyCredentialsMatcher myCredentialsMatcher; @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager manager,JwtFilter jwtFilter){ ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean(); filterFactoryBean.setSecurityManager(manager); //设置jwt过滤器 Map<String, Filter> map=new LinkedHashMap<>(); map.put("jwt",jwtFilter); filterFactoryBean.setFilters(map); ? //权限访问规则 Map<String, String> filterMap=new LinkedHashMap<>(); filterMap.put("/api/user/login","anon");//不拦截登录页面 filterMap.put("/api/user/**","authc"); //访问user下面的页面 需要authc权限 filterMap.put("/**","jwt");//统一经过jwt过滤器 filterFactoryBean.setLoginUrl("/error/unLogin");//没有登录跳转到这个地址 filterFactoryBean.setFilterChainDefinitionMap(filterMap); return filterFactoryBean; } @Bean(name="securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); userRealm.setCredentialsMatcher(myCredentialsMatcher); //绑定realm securityManager.setRealm(userRealm); return securityManager; } //创建jwt过滤器 上面创建的 @Bean public JwtFilter getFilter(){ return new JwtFilter(); } // 开启注解代理 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } @Bean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); creator.setProxyTargetClass(true); return creator; } }
自定义密码验证器
因为我们这里用的自定义的token ,原有的那个密码验证器无法解析我们的token中的信息
让shiro走我们自定义的密码验证器(从这里也可以做一些加密的比对)
//自定义密码校验器 @Component public class MyCredentialsMatcher extends SimpleCredentialsMatcher { @Autowired private UserService userService; //使用自定义token @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { JwtToken jwtToken=(JwtToken) token; //这里主要是为了防止密码为空时候报异常,我们这里应该在前端约束密码不能为空 if (jwtToken.getPassword() == null){ return true; } //获得用户输入的密码: String inPassword = new String(jwtToken.getPassword()); //获得用户输入的密码: String username = String.valueOf(info.getPrincipals()); //获得数据库中的密码 String dbPassword=(String) info.getCredentials(); //获取盐 User user = userService.getUser(username); //进行密码的比对 return this.equals(inPassword, dbPassword); } ? }
统一异常处理
@RestControllerAdvice @Slf4j public class ShiroException { @ExceptionHandler(value = UnauthorizedException.class) public Result handler(){ return Result.fail("权限不足"); } @ExceptionHandler(value = ExpiredCredentialsException.class) public Result handler(ExpiredCredentialsException e) { log.error("运行时异常:----------------{}", e.getMessage()); return Result.fail("登录已过期,请重新登录"); } @ExceptionHandler(value = UnauthenticatedException.class) public Result handler(UnauthenticatedException e) { log.error("运行时异常:----------------{}", e); return Result.fail("未登录"); } @ExceptionHandler(value = UnknownAccountException.class) public Result handler(UnknownAccountException e) { log.error("运行时异常:----------------{}", e); return Result.fail("未登录"); } }