前后端分離后端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("未登錄"); } }