vue+springboot+shiro+jwt實(shí)現(xiàn)登錄功能
公司開(kāi)發(fā)的系統(tǒng)原先的用戶信息是基于shiro session 進(jìn)行管理,但是session不適用于app端,并且服務(wù)器重啟后需要重新登錄。需要改造將shiro和jwt進(jìn)行整合,實(shí)現(xiàn)通過(guò)token登錄。
1.導(dǎo)入依賴
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.2.0</version> </dependency>
2.JWTToken 替換 Shiro 原生 Token
import org.apache.shiro.authc.AuthenticationToken; public class JWTToken implements AuthenticationToken { // 密鑰 private String token; public JWTToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; public Object getCredentials() { }
3.JWT token 工具類,提供JWT生成、校驗(yàn)、獲取token存儲(chǔ)的信息
import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.interfaces.DecodedJWT; import com.hxkg.datafusion.controller.FlieController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.io.UnsupportedEncodingException; import java.util.Date; /** * JWT token 工具類 * * @author yangfeng * @Date 2019-09-17 */ @ConfigurationProperties(prefix = "jwt") @Component public class JWTUtil { private static Logger LOG = LoggerFactory.getLogger(FlieController.class); //過(guò)期時(shí)間 private static Long expire; // 秘鑰 private static String secret; /** * 校驗(yàn)token是否正確 * * @param token 密鑰 * @return 是否正確 */ public static boolean verify(String token, String userName) { try { //根據(jù)密碼生成JWT效驗(yàn)器 Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm) .withClaim("userName", userName) .build(); //效驗(yàn)TOKEN verifier.verify(token); return true; } catch (UnsupportedEncodingException e) { return false; } } * 獲得token中的信息無(wú)需secret解密也能獲得 * @return token中包含的用戶名 public static String getUsername(String token) { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("userName").asString(); } catch (JWTDecodeException e) { return null; * 獲取用戶id * @param token * @return public static String getUserId(String token) { return jwt.getClaim("userId").asString(); * 生成簽名 * @param userName 用戶名 * @param userId * @return 加密的token public static String sign(String userName, String userId) { Date date = new Date(System.currentTimeMillis() + expire * 1000); // 附帶username信息 return JWT.create() .withClaim("userId", userId) .withExpiresAt(date) .sign(algorithm); } catch (Exception e) { public static Long getExpire() { return expire; public static void setExpire(Long expire) { JWTUtil.expire = expire; public static String getSecret() { return secret; public static void setSecret(String secret) { JWTUtil.secret = secret; }
application.properties中增加:
#jwt token # token有效時(shí)長(zhǎng),7天,單位秒 jwt.expire=604800 jwt.secret=JWT_TOKEN_SHIRO
注意:jwt加解密的私鑰使用配置的字符串而不使用用戶登錄密碼的好處是防止用戶密碼被其他人修改后,用戶操作系統(tǒng)此時(shí)再去校驗(yàn)token會(huì)發(fā)生token解析對(duì)不上的情況。
4.JWTFilter請(qǐng)求攔截
import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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; /** * jwt過(guò)濾器 * * @Author yangfeng * @Description preHandle->isAccessAllowed->isLoginAttempt->executeLogin * @Date 2019-09-18 * @Time 12:36 */ public class JWTFilter extends BasicHttpAuthenticationFilter { private Logger logger = LoggerFactory.getLogger(this.getClass()); /** * 如果帶有 token,則對(duì) token 進(jìn)行檢查,否則直接通過(guò) */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException { //判斷請(qǐng)求的請(qǐng)求頭是否帶上 "Token" if (isLoginAttempt(request, response)) { //如果存在,則進(jìn)入 executeLogin 方法執(zhí)行登入,檢查 token 是否正確 try { executeLogin(request, response); return true; } catch (Exception e) { //token 錯(cuò)誤 responseError(response, e.getMessage()); return false; //產(chǎn)生異常則阻止請(qǐng)求的繼續(xù)執(zhí)行 } } //如果請(qǐng)求頭不存在 Token,則可能是執(zhí)行登陸操作或者是游客狀態(tài)訪問(wèn),無(wú)需檢查 token,直接返回 true return true; } /** * 判斷用戶是否想要登入。 * 檢測(cè) header 里面是否包含 Token */ @Override protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { HttpServletRequest req = (HttpServletRequest) request; String token = req.getHeader("Authorization"); return token != null; } /** * 執(zhí)行登陸操作 */ @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = httpServletRequest.getHeader("Authorization"); JWTToken jwtToken = new JWTToken(token); // 提交給realm進(jìn)行登入,如果錯(cuò)誤他會(huì)拋出異常并被捕獲 getSubject(request, response).login(jwtToken); // 如果沒(méi)有拋出異常則代表登入成功,返回true return true; } /** * 對(duì)跨域提供支持 */ @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; httpServletResponse.setHeader("Access-control-Allow-Origin", "*"); httpServletResponse.setHeader("Access-Control-Allow-Methods", "*"); httpServletResponse.setHeader("Access-Control-Allow-Headers", "*"); // 跨域時(shí)會(huì)首先發(fā)送一個(gè)option請(qǐng)求,這里我們給option請(qǐng)求直接返回正常狀態(tài) if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response); } /** * 請(qǐng)求異常跳轉(zhuǎn) */ private void responseError(ServletResponse response, String message) { try { HttpServletResponse httpServletResponse = (HttpServletResponse) response; //設(shè)置編碼,否則中文字符在重定向時(shí)會(huì)變?yōu)榭兆址? message = URLEncoder.encode(message, "UTF-8"); httpServletResponse.sendRedirect("/pc/login/noLogin?message=" + message); } catch (IOException e) { logger.error(e.getMessage()); } } }
executeLogin()
方法中的getSubject(request, response).login(jwtToken)
就是觸發(fā)Shiro 登錄操作。
5.登錄授權(quán)realm
import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.exceptions.TokenExpiredException; import com.hxkg.datafusion.common.jwt.JWTToken; import com.hxkg.datafusion.common.jwt.JWTUtil; import com.hxkg.datafusion.entity.customized.RoleAO; import com.hxkg.datafusion.entity.customized.UserAO; import com.hxkg.datafusion.entity.customized.VUserPrivilegeAO; import com.hxkg.datafusion.service.IRoleService; import com.hxkg.datafusion.service.IUserService; import com.hxkg.datafusion.service.IVUserPrivilegeService; import org.apache.shiro.SecurityUtils; 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.Service; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.util.List; /** * 登錄授權(quán)realm * * @author yangfeng * @date 2019.6.21 */ @Service public class ShiroRealm extends AuthorizingRealm { @Resource private IRoleService roleService; @Resource private IVUserPrivilegeService vUserPrivilegeService; @Resource private IUserService userService; @Override public boolean supports(AuthenticationToken token) { return token instanceof JWTToken; } /** * 為當(dāng)前登錄用戶授予角色和權(quán)限 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = JWTUtil.getUsername(principals.getPrimaryPrincipal().toString()); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // 根據(jù)登錄名獲取登用戶信息 if (!StringUtils.isEmpty(username)) { //設(shè)置用戶角色 ServiceResult<List<RoleAO>> rolesResult = roleService.getRolesByUserName(username); if (rolesResult != null && rolesResult.isSucceed() && !CollectionUtils.isEmpty(rolesResult.getData())) { for (RoleAO role : rolesResult.getData()) { authorizationInfo.addRole(role.getName()); } } //設(shè)置權(quán)限 List<VUserPrivilegeAO> privileges = vUserPrivilegeService.queryPrivilegeByUserName(username); if (!CollectionUtils.isEmpty(privileges)) { for (VUserPrivilegeAO privilege : privileges) { if (privilege == null || StringUtils.isEmpty(privilege.getPrivilegecode())) { continue; } //權(quán)限操作代碼 authorizationInfo.addStringPermission(privilege.getPrivilegecode()); } } return authorizationInfo; } return null; } /** * 驗(yàn)證當(dāng)前登錄的用戶 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { String token = (String) auth.getCredentials(); // 解密獲得username,用于和數(shù)據(jù)庫(kù)進(jìn)行對(duì)比 String username = JWTUtil.getUsername(token); if (StringUtils.isEmpty(username)) { throw new AuthenticationException("token錯(cuò)誤!"); } UserAO user = userService.getUserByName(username); if (user == null) { throw new AuthenticationException("用戶不存在!"); } if (Constant.DISABLE.equals(user.getEnabled())) { throw new AuthenticationException("賬號(hào)已被禁用!"); } try { if (JWTUtil.verify(token, username)) { return new SimpleAuthenticationInfo(token, token, getName()); } else { throw new AuthenticationException("token認(rèn)證失敗!"); } } catch (TokenExpiredException e) { throw new AuthenticationException("token已過(guò)期!"); } catch (SignatureVerificationException e) { throw new AuthenticationException("密碼不正確!"); } } /** * 清除登陸用戶授權(quán)信息緩存. */ public void clearCached() { this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals()); } /** * 超級(jí)管理員不做權(quán)限的判斷,自動(dòng)擁有所有權(quán)限 * * @param principals * @param permission * @return */ @Override public boolean isPermitted(PrincipalCollection principals, String permission) { String username = JWTUtil.getUsername(principals.getPrimaryPrincipal().toString()); //Constant常量文件配置: //public static String SYSTEM_SUPER_ADMIN = "admin";系統(tǒng)超級(jí)管理員 return Constant.SYSTEM_SUPER_ADMIN.equals(username) || super.isPermitted(principals, permission); } @Override public boolean hasRole(PrincipalCollection principals, String roleIdentifier) { String username = JWTUtil.getUsername(principals.getPrimaryPrincipal().toString()); return Constant.SYSTEM_SUPER_ADMIN.equals(username) || super.hasRole(principals, roleIdentifier); } }
6.shiro配置
import com.hxkg.datafusion.common.jwt.JWTFilter; import com.hxkg.datafusion.util.ShiroRealm; 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.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import javax.servlet.Filter; import java.util.HashMap; import java.util.Map; /** * shiro配置 * * @author yangfeng * @date 2019.7.14 */ @Configuration public class ShiroConfig { @Bean("securityManager") public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 使用自己的realm securityManager.setRealm(shiroRealm); /* * 關(guān)閉shiro自帶的session,詳情見(jiàn)文檔 * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29 */ DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); securityManager.setSubjectDAO(subjectDAO); return securityManager; } @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); // 添加自己的過(guò)濾器并且取名為jwt Map<String, Filter> filterMap = new HashMap<>(); filterMap.put("jwt", new JWTFilter()); factoryBean.setFilters(filterMap); factoryBean.setSecurityManager(securityManager); factoryBean.setUnauthorizedUrl("/401"); /* * 自定義url規(guī)則 * http://shiro.apache.org/web.html#urls- */ Map<String, String> filterRuleMap = new HashMap<>(); filterRuleMap.put("/pc/login/doLogin", "anon"); filterRuleMap.put("/pc/login/logout", "anon"); filterRuleMap.put("/pc/login/noLogin", "anon"); filterRuleMap.put("/**", "authc"); // 所有請(qǐng)求通過(guò)我們自己的JWT Filter filterRuleMap.put("/**", "jwt"); factoryBean.setLoginUrl("/pc/login/noLogin");//沒(méi)有登錄的用戶請(qǐng)求需要登錄的資源時(shí)自動(dòng)跳轉(zhuǎn)到該路徑 factoryBean.setUnauthorizedUrl("/pc/login/unauthorized");//沒(méi)有權(quán)限默認(rèn)跳轉(zhuǎn) factoryBean.setFilterChainDefinitionMap(filterRuleMap); return factoryBean; } /** * 開(kāi)啟Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP掃描使用Shiro注解的類,并在必要時(shí)進(jìn)行安全邏輯驗(yàn)證 * @param securityManager * @return */ /** * 下面的代碼是添加注解支持 */ @Bean @DependsOn("lifecycleBeanPostProcessor") public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } } 除了登錄doLogin、退出logout、沒(méi)有登錄跳轉(zhuǎn)登錄noLogin方法外,其他所有請(qǐng)求通過(guò)我們自己的JWT Filter。
7.登錄web端
import com.hxkg.datafusion.common.jwt.JWTUtil; import com.hxkg.datafusion.entity.customized.UserAO; import com.hxkg.datafusion.entity.customized.VUserPrivilegeAO; import com.hxkg.datafusion.service.IUserService; import com.hxkg.datafusion.service.IVUserPrivilegeService; import com.hxkg.datafusion.util.*; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Date; import java.util.List; /** * 登錄 * * @author yangfeng * @date 2019-07-11 16:02 */ @RestController @RequestMapping(value = "pc/login") public class LoginController { private static Logger LOG = LoggerFactory.getLogger(LoginController.class); @Resource private IUserService userService; @Resource private IVUserPrivilegeService vUserPrivilegeService; /** * 未登陸認(rèn)證 * * @param message * @return */ @RequestMapping(value = "noLogin", method = {RequestMethod.GET, RequestMethod.POST}) @ResponseBody public Object noLogin(String message) { ServiceResult<String> genResult = ServiceResultHelper.genResult(false, Constant.NO_AUTHENTICATION, StringUtils.isNotEmpty(message) ? message : "用戶登錄信息已失效,請(qǐng)重新登錄后再試。", null); return genResult; } /** * 未授權(quán) * * @return */ @RequestMapping(value = "unauthorized", method = {RequestMethod.GET, RequestMethod.POST}) public Object unauthorized() { return ServiceResultHelper.genResult(false, Constant.ErrorCode.PERMISSION_DENIED_CODE, Constant.ErrorCode.PERMISSION_DENIED_MSG, null); } /** * 登錄操作 * * @param username * @param password * @param request * @param response * @return */ @RequestMapping(value = "doLogin", method = {RequestMethod.GET, RequestMethod.POST}) public Object doLogin(String username, String password, Boolean rememberMe, HttpServletRequest request, HttpServletResponse response) { ServiceResult<UserAO> genResult; // 獲取鹽值 String salt; UserAO loginUser = userService.getUserByName(username); if (loginUser == null) { genResult = ServiceResultHelper.genResult(false, Constant.ErrorCode.USER_NOT_EXIST_ERROR, Constant.ErrorCode.USER_NOT_EXIST_ERROR_MSG, null); return genResult; } if (Constant.DISABLE.equals(loginUser.getEnabled())) { genResult = ServiceResultHelper.genResult(false, Constant.ErrorCode.USER_DISABLE_ERROR, Constant.ErrorCode.USER_DISABLE_ERROR_MSG, null); return genResult; } salt = loginUser.getSalt(); password = MD5Util.MD5Encode(password + salt); UserAO user = userService.getUserByNameAndPwd(username, password); if (user != null) { genResult = ServiceResultHelper.genResult(true, Constant.SUCCESS, "登錄成功", user); //獲取權(quán)限 List<VUserPrivilegeAO> userPrivileges = vUserPrivilegeService.queryPrivilegeByUserName(username); user.setPrivileges(userPrivileges); //更新登錄信息 user.setLastLoginTime(DateTimeUtil.format(new Date(), DateTimeUtil.YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)); user.setLastLoginIp(IPUtil.getClientIp(request)); userService.saveOrUpdate(user); genResult.setAdditionalProperties("token", JWTUtil.sign(username, user.getId())); } else { genResult = ServiceResultHelper.genResult(false, Constant.ErrorCode.PASSWORD_ERROR, Constant.ErrorCode.PASSWORD_ERROR_MSG, null); } return genResult; } /** * 退出登錄 * * @param request * @param response * @return */ @RequestMapping(value = "logout", method = {RequestMethod.GET, RequestMethod.POST}) public Object logout(HttpServletRequest request, HttpServletResponse response) { Subject currentUser = SecurityUtils.getSubject(); currentUser.logout(); CookieUtil cookieUtil = new CookieUtil(request, response, 0); cookieUtil.deleteCookie(Constant.SESSION_CURRENT_USER); cookieUtil.deleteCookie("JSESSIONID"); ServiceResult<Object> ret = new ServiceResult<Object>(); ret.setMsg("退出登錄成功"); ret.setCode(Constant.SUCCESS); ret.setData(null); ret.setSucceed(true); return ret; } }
登錄完成一系列的檢查,成功后創(chuàng)建jwt token。
8.異常處理
import com.hxkg.datafusion.util.Constant; import com.hxkg.datafusion.util.ServiceResultHelper; import org.apache.shiro.authz.UnauthenticatedException; import org.apache.shiro.authz.UnauthorizedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 異常處理 * * @author yangfeng * @date 2019-09-11 */ @ControllerAdvice public class WebExceptionHandler { private static Logger LOG = LoggerFactory.getLogger(WebExceptionHandler.class); @ExceptionHandler(UnauthorizedException.class) @ResponseBody public Object handleUnauthorizedException(Exception ex, HttpServletRequest request, HttpServletResponse response) { LOG.error("{}", ex.getMessage()); return ServiceResultHelper.genResult(false, Constant.ErrorCode.PERMISSION_DENIED_CODE, Constant.ErrorCode.PERMISSION_DENIED_MSG, null); } @ExceptionHandler(UnauthenticatedException.class) @ResponseBody public Object handleUnauthenticatedException(Exception ex, HttpServletRequest request, HttpServletResponse response) { LOG.error("{}", ex.getMessage()); return ServiceResultHelper.genResult(false, Constant.ErrorCode.INVALID_LOGIN_CODE, Constant.ErrorCode.INVALID_LOGIN_MSG, null); } @ExceptionHandler(Exception.class) @ResponseBody public Object handleException(Exception ex, HttpServletRequest request, HttpServletResponse response) { LOG.error("{}", ex.getMessage()); return ServiceResultHelper.genResult(false, Constant.ErrorCode.SERVER_ERROR_CODE, Constant.ErrorCode.SERVER_ERROR_MSG, null); } }
處理未登錄、未授權(quán)等異常,返回相應(yīng)的代碼,方便前端捕獲跳轉(zhuǎn)登錄頁(yè)面,或者作出提示,而不會(huì)直接拋出服務(wù)器異常導(dǎo)致提示不夠明確。
到此后端已經(jīng)全部寫(xiě)完。接下來(lái)是vue部分。
9.緩存調(diào)用登錄接口傳過(guò)來(lái)的token
localStorage.setItem('token', token);
10.請(qǐng)求頭設(shè)置,帶上token
//封裝請(qǐng)求 function xAxios(options) { let opts = {...options}; let token = localStorage.getItem('token') if (token) { opts.headers['Authorization'] = token }
11.生產(chǎn)環(huán)境nginx配置
因?yàn)楹蠖薺etFilter登錄異常會(huì)進(jìn)行重定向,所以nginx需要加上前端的代理配置。
到此這篇關(guān)于vue+springboot+shiro+jwt實(shí)現(xiàn)登錄的文章就介紹到這了,更多相關(guān)springboot jwt實(shí)現(xiàn)登錄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
通過(guò)簡(jiǎn)單方法實(shí)現(xiàn)spring boot web項(xiàng)目
這篇文章主要介紹了通過(guò)簡(jiǎn)單方法實(shí)現(xiàn)spring boot web項(xiàng)目,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09Java實(shí)現(xiàn)Word/Pdf/TXT轉(zhuǎn)html的實(shí)例代碼
本文主要介紹了Java實(shí)現(xiàn)Word/Pdf/TXT轉(zhuǎn)html的實(shí)例代碼,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-02-02Java修改maven的默認(rèn)jdk版本為1.7的方法
這篇文章主要介紹了Java修改maven的默認(rèn)jdk版本為1.7的方法,需要的朋友可以參考下2018-02-02后端返回各種圖片形式在前端的轉(zhuǎn)換及展示方法對(duì)比
這篇文章主要給大家介紹了關(guān)于后端返回各種圖片形式在前端的轉(zhuǎn)換及展示方法對(duì)比的相關(guān)資料,文中通過(guò)圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-06-06Spring Boot利用Thymeleaf發(fā)送Email的方法教程
spring Boot默認(rèn)就是使用thymeleaf模板引擎的,下面這篇文章主要給大家介紹了關(guān)于在Spring Boot中利用Thymeleaf發(fā)送Email的方法教程,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-08-08