SpringSecurity實(shí)現(xiàn)登陸認(rèn)證并返回token方式
一丶SpringSecurity+oauth2(密碼模式)方式進(jìn)行認(rèn)證授權(quán)
1.新增自定義配置類
實(shí)現(xiàn)WebSecurityConfigurerAdapter
重寫其中的configure(HttpSecurity http)方法
配置登陸頁(yè)面以及登陸請(qǐng)求url等參數(shù)
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationSuccessHandler authenticationSuccessHandler; @Autowired(required = false) private AuthenticationEntryPoint authenticationEntryPoint; @Resource private UserDetailsService userDetailsService; @Autowired private PasswordEncoder passwordEncoder; @Resource private LogoutHandler oauthLogoutHandler; @Autowired private OpenIdAuthenticationSecurityConfig openIdAuthenticationSecurityConfig; @Autowired private MobileAuthenticationSecurityConfig mobileAuthenticationSecurityConfig; @Autowired private MobileAuthrnticationCodeSecurityConfig mobileAuthrnticationCodeSecurityConfig; @Autowired private PasswordAuthrnticationCodeSecurityConfig passwordAuthrnticationCodeSecurityConfig; @Autowired private AuthenticationManager authenticationManager; @Autowired private TenantAuthenticationSecurityConfig tenantAuthenticationSecurityConfig; @Autowired private TenantProperties tenantProperties; /** * 這一步的配置是必不可少的,否則SpringBoot會(huì)自動(dòng)配置一個(gè)AuthenticationManager,覆蓋掉內(nèi)存中的用戶 * @return 認(rèn)證管理對(duì)象 */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest() //授權(quán)服務(wù)器關(guān)閉basic認(rèn)證 .permitAll() .and() .logout() .logoutUrl(SecurityConstants.LOGOUT_URL) .logoutSuccessHandler(new OauthLogoutSuccessHandler()) .addLogoutHandler(oauthLogoutHandler) .clearAuthentication(true) .and() .apply(openIdAuthenticationSecurityConfig) .and() .apply(mobileAuthenticationSecurityConfig) .and() .apply(passwordAuthrnticationCodeSecurityConfig) .and() .apply(mobileAuthrnticationCodeSecurityConfig) .and() .addFilterBefore(new LoginProcessSetTenantFilter(), UsernamePasswordAuthenticationFilter.class) .csrf().disable() // 解決不允許顯示在iframe的問題 .headers().frameOptions().disable().cacheControl(); http.formLogin() .loginPage(SecurityConstants.LOGIN_PAGE) .loginProcessingUrl(SecurityConstants.OAUTH_LOGIN_PRO_URL) .successHandler(authenticationSuccessHandler); // 基于密碼 等模式可以無(wú)session,不支持授權(quán)碼模式 if (authenticationEntryPoint != null) { http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint); http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } else { // 授權(quán)碼模式單獨(dú)處理,需要session的支持,此模式可以支持所有oauth2的認(rèn)證 http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED); } } }
2.OAuth2 授權(quán)服務(wù)器配置
@Configuration @EnableAuthorizationServer @AutoConfigureAfter(AuthorizationServerEndpointsConfigurer.class) public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { /** * 注入authenticationManager 來(lái)支持 password grant type */ @Autowired private AuthenticationManager authenticationManager; @Resource private UserDetailsService userDetailsService; @Autowired private TokenStore tokenStore; @Autowired private WebResponseExceptionTranslator webResponseExceptionTranslator; @Autowired private RedisClientDetailsService clientDetailsService; @Autowired private RandomValueAuthorizationCodeServices authorizationCodeServices; @Autowired private TokenGranter tokenGranter; /** * 配置身份認(rèn)證器,配置認(rèn)證方式,TokenStore,TokenGranter,OAuth2RequestFactory * @param endpoints */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { endpoints.tokenStore(tokenStore) .authenticationManager(authenticationManager) .userDetailsService(userDetailsService) .authorizationCodeServices(authorizationCodeServices) .exceptionTranslator(webResponseExceptionTranslator) .tokenGranter(tokenGranter); } /** * 配置應(yīng)用名稱 應(yīng)用id * 配置OAuth2的客戶端相關(guān)信息 * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(clientDetailsService); clientDetailsService.loadAllClientToCache(); } /** * 對(duì)應(yīng)于配置AuthorizationServer安全認(rèn)證的相關(guān)信息,創(chuàng)建ClientCredentialsTokenEndpointFilter核心過濾器 * @param security */ @Override public void configure(AuthorizationServerSecurityConfigurer security) { security .tokenKeyAccess("isAuthenticated()") .checkTokenAccess("permitAll()") //讓/oauth/token支持client_id以及client_secret作登錄認(rèn)證 .allowFormAuthenticationForClients(); } }
3.前端部分代碼
需要傳入grant_type,deviceId,username,password
例如請(qǐng)求是這樣的:
https://oauth.b.com/oauth/token? grant_type=password& # 授權(quán)方式是"密碼式" username=USERNAME& password=PASSWORD& client_id=CLIENT_ID& client_secret=123123& scope=all
//賬號(hào)密碼登陸 var grant_type = 'password_code'; var deviceId = $("input[name=deviceId]").val(); var validCode = $("input[name=validCode]").val(); layer.load(2); var clients = $("#clients").attr("value"); var loginData ={"clients":clients,"grant_type":grant_type,"username":username,"password":hex_md5(password) ,"deviceId":deviceId,"validCode":validCode} config.putApp(clients); $.ajax({ url: config.base_server + '/oauth/token', xhrFields: { withCredentials: true }, data: loginData, type: 'POST', beforeSend: function (xhr) { xhr.setRequestHeader('Authorization', 'Basic ' + window.btoa(config.clientId + ":" + config.clientSecret)); }, success: function (data) { if (data.resp_code === 0) { config.putToken(data.datas); layer.msg('登錄成功', {icon: 1, time: 500}, function () { location.replace('./'); }); } else { layer.closeAll('loading'); layer.msg(data.resp_msg, {icon: 5, time: 500}); } }, error: function (xhr) { layer.closeAll('loading'); //區(qū)分錯(cuò)誤信息 //驗(yàn)證碼錯(cuò)誤 if(xhr.responseJSON.error === 'invalid_grant'){ layer.msg(xhr.responseJSON.resp_msg, {icon: 5, time: 500}); }else if(xhr.responseJSON.error === 'unsupported_response_type'){ //賬號(hào)錯(cuò)誤 var win = layer.open({ content:'<div>該賬號(hào)已經(jīng)被系統(tǒng)鎖定或者禁用,<br>如需幫助請(qǐng)及時(shí)聯(lián)系系統(tǒng)管理員進(jìn)行處理!</div>' ,btn: ['確定'] ,btnAlign: 'c' ,closeBtn: 0 ,yes: function(index, layero){ layer.close(win); } }); }else { layer.msg(xhr.responseJSON.resp_msg, {icon: 5, time: 500}); } var src = $(".login-code").attr("src"); $(".login-code").attr("src", src + '?t=' + (new Date).getTime()); } }); //阻止表單跳轉(zhuǎn) return false;
4.如果需要其他自定義的授權(quán)模式
可以新增一個(gè)配置類。
@Configuration public class TokenGranterConfig { @Autowired private ClientDetailsService clientDetailsService; @Autowired private UserDetailsService userDetailsService; @Autowired private AuthenticationManager authenticationManager; @Autowired private TokenStore tokenStore; @Autowired(required = false) private List<TokenEnhancer> tokenEnhancer; @Autowired private IValidateCodeService validateCodeService; @Autowired private RandomValueAuthorizationCodeServices authorizationCodeServices; private boolean reuseRefreshToken = true; private AuthorizationServerTokenServices tokenServices; private TokenGranter tokenGranter; @Autowired private UserService userService; /** * 是否登錄同應(yīng)用同賬號(hào)互踢 */ @Value("${zlt.uaa.isSingleLogin:false}") private boolean isSingleLogin; /** * 授權(quán)模式 */ @Bean public TokenGranter tokenGranter() { if (tokenGranter == null) { tokenGranter = new TokenGranter() { private CompositeTokenGranter delegate; @Override public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) { if (delegate == null) { delegate = new CompositeTokenGranter(getAllTokenGranters()); } return delegate.grant(grantType, tokenRequest); } }; } return tokenGranter; } /** * 所有授權(quán)模式:默認(rèn)的5種模式 + 自定義的模式 */ private List<TokenGranter> getAllTokenGranters() { AuthorizationServerTokenServices tokenServices = tokenServices(); AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices(); OAuth2RequestFactory requestFactory = requestFactory(); //獲取默認(rèn)的授權(quán)模式 List<TokenGranter> tokenGranters = getDefaultTokenGranters(tokenServices, authorizationCodeServices, requestFactory); if (authenticationManager != null) { //添加手機(jī)號(hào)加驗(yàn)證碼 tokenGranters.add(new MobileCodeGranter(authenticationManager,tokenServices,clientDetailsService,requestFactory, validateCodeService)); // 添加密碼加圖形驗(yàn)證碼模式 tokenGranters.add(new PwdImgCodeGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory, validateCodeService,userService)); // 添加openId模式 tokenGranters.add(new OpenIdGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory)); // 添加手機(jī)號(hào)加密碼授權(quán)模式 tokenGranters.add(new MobilePwdGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory)); tokenGranters.add(new PwdGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory,userService)); } return tokenGranters; } /** * 默認(rèn)的授權(quán)模式 */ private List<TokenGranter> getDefaultTokenGranters(AuthorizationServerTokenServices tokenServices , AuthorizationCodeServices authorizationCodeServices, OAuth2RequestFactory requestFactory) { List<TokenGranter> tokenGranters = new ArrayList<>(); // 添加授權(quán)碼模式 tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetailsService, requestFactory)); // 添加刷新令牌的模式 tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetailsService, requestFactory)); // 添加隱士授權(quán)模式 tokenGranters.add(new ImplicitTokenGranter(tokenServices, clientDetailsService, requestFactory)); // 添加客戶端模式 tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetailsService, requestFactory)); if (authenticationManager != null) { // 添加密碼模式 tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory)); } return tokenGranters; } private AuthorizationServerTokenServices tokenServices() { if (tokenServices != null) { return tokenServices; } this.tokenServices = createDefaultTokenServices(); return tokenServices; } private AuthorizationCodeServices authorizationCodeServices() { if (authorizationCodeServices == null) { authorizationCodeServices = new InMemoryAuthorizationCodeServices(); } return authorizationCodeServices; } private OAuth2RequestFactory requestFactory() { return new DefaultOAuth2RequestFactory(clientDetailsService); } private DefaultTokenServices createDefaultTokenServices() { DefaultTokenServices tokenServices = new CustomTokenServices(isSingleLogin); tokenServices.setTokenStore(tokenStore); tokenServices.setSupportRefreshToken(true); tokenServices.setReuseRefreshToken(reuseRefreshToken); tokenServices.setClientDetailsService(clientDetailsService); tokenServices.setTokenEnhancer(tokenEnhancer()); addUserDetailsService(tokenServices, this.userDetailsService); return tokenServices; } private TokenEnhancer tokenEnhancer() { if (tokenEnhancer != null) { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); tokenEnhancerChain.setTokenEnhancers(tokenEnhancer); return tokenEnhancerChain; } return null; } private void addUserDetailsService(DefaultTokenServices tokenServices, UserDetailsService userDetailsService) { if (userDetailsService != null) { PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider(); provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<>(userDetailsService)); tokenServices.setAuthenticationManager(new ProviderManager(Collections.singletonList(provider))); } } }
二丶SpringSecurity+自定義登陸控制器(使用最多)
1.自定義類繼承
CustomWebSecurityConfigurerAdapter,配置請(qǐng)求過濾路徑等
@Configuration //@EnableWebSecurity public class CustomWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @Autowired private MemoryCacheSecurityContextRepository memoryCacheSecurityContextRepository; @Autowired private CustomAuthenticatedSessionStrategy customAuthenticatedSessionStrategy; @Autowired private CustomLogoutHandler customLogoutHandler; @Autowired private CustomAuthenticationFailureHandler customAuthenticationFailureHandler; @Autowired private CustomAuthenticationEntryPoint customAuthenticationEntryPoint; @Override protected void configure(HttpSecurity http) throws Exception { http // .requestMatcher(EndpointRequest.toAnyEndpoint()).authorizeRequests() // .anyRequest().hasRole("ENDPOINT_ADMIN") // .and() .authorizeRequests() .antMatchers("/api/public/**") .permitAll().and().authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN").and() .authorizeRequests().antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')").and() .authorizeRequests().anyRequest().authenticated().and().formLogin() .defaultSuccessUrl("/newweb/templates/admin_grid.html", true) // .loginPage("/login") .failureHandler(customAuthenticationFailureHandler).and().logout() // .logoutUrl("/logout") // .logoutSuccessUrl("/newweb/templates/admin_login.html") // .logoutSuccessHandler(new CustomLogoutSuccessHandler()) .invalidateHttpSession(true).addLogoutHandler(customLogoutHandler) .deleteCookies(SecurityConstants.SECURITY_TOKEN_KEY).and(); // 不創(chuàng)建session http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 自定義安全上下文倉(cāng)庫(kù),覆蓋默認(rèn)的httpsession實(shí)現(xiàn) http.securityContext().securityContextRepository(memoryCacheSecurityContextRepository); // 認(rèn)證成功之后,不進(jìn)行http session相關(guān)處理 http.sessionManagement().sessionAuthenticationStrategy(customAuthenticatedSessionStrategy); http.csrf().disable(); // http.exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint); } protected void configure(AuthenticationManagerBuilder auth) throws Exception { super.configure(auth); } }
2.登陸控制器
做一些驗(yàn)證碼處理和賬號(hào)密碼驗(yàn)證等操作
@RestController @RequestMapping("/api/oauth") public class LoginController { @Autowired private UserService userService; @Autowired private TokenEndpoint tokenEndpoint; @Autowired private LoginService loginService; @Autowired private UserProvider userProvider; @Autowired private ConfigValueUtil configValueUtil; @Autowired private RedisUtil redisUtil; @Autowired private ExpertInfoService expertInfoService; @Autowired private RoleService roleService; @ApiOperation("登陸(切換登錄模式需請(qǐng)清空l(shuí)oginForm中的值)") @PostMapping("/Login") public ActionResult<LoginVO> login(Principal principal, @RequestParam Map<String, String> parameters, @RequestBody LoginForm loginForm) throws LoginException { TenantContextHolder.clear(); UserInfo userInfo = new UserInfo(); String phone = loginForm.getPhone(); String phoneCode = loginForm.getPhoneCode(); String timestampkey = loginForm.getTimestamp(); if(StringUtil.isNotEmpty(phone)){ List<UserEntity> userEntityList = userService.list(new QueryWrapper<UserEntity>().lambda().eq(UserEntity::getMobilePhone,phone)); if(CollectionUtils.isNotEmpty(userEntityList)){ String phoneCode1 = String.valueOf(redisUtil.getString(phone)); if("null".equals(phoneCode1)){ throw new LoginException("驗(yàn)證碼已過期!"); } if(!(phoneCode1.equals(phoneCode))){ throw new LoginException("驗(yàn)證碼輸入錯(cuò)誤!"); } if(StringUtil.isNotEmpty(loginForm.getAccount())){ userEntityList = userEntityList.stream().filter(t->loginForm.getAccount().equals(t.getAccount())).collect(Collectors.toList()); } if(userEntityList.size() > 1){ List<UserLoginForm> userLoginFormList = JsonUtil.getJsonToList(userEntityList,UserLoginForm.class); LoginVO loginVO = new LoginVO(); loginVO.setUserLogFormList(userLoginFormList); return ActionResult.success(loginVO); } UserEntity userEntity = userEntityList.get(0); loginForm.setAccount(userEntity.getAccount()); loginForm.setPassword(userEntity.getPassword()); redisUtil.remove(phone); } }else{ String code = loginForm.getCode(); String timestamp = String.valueOf(redisUtil.getString(timestampkey)); if("null".equals(timestamp)){ throw new LoginException("驗(yàn)證碼已過期!"); } if(!(code).equalsIgnoreCase(timestamp)){ throw new LoginException("驗(yàn)證碼錯(cuò)誤!"); } } loginService.isExistUser(loginForm.getAccount().trim(), loginForm.getPassword().trim()); List<UserEntity> userEntityList = userService.getUserEntitys(StringUtil.isNotEmpty( loginForm.getPhonePassword())?loginForm.getPhonePassword():loginForm.getAccount()); UserEntity entity = new UserEntity(); if(userEntityList.size() > 1){ for (UserEntity item : userEntityList) { if(item.getPassword().equals(Md5Util.getStringMd5(loginForm.getPassword() + item.getSecretkey().toLowerCase()))){ if(StringUtil.isNotEmpty(loginForm.getPhonePassword())){ entity = userEntityList.stream().filter(t->loginForm.getAccount().equals(t.getAccount())).collect(Collectors.toList()).get(0); loginForm.setAccount(entity.getAccount()); loginForm.setPassword(entity.getPassword()); }else{ List<UserLoginForm> userLoginFormList = JsonUtil.getJsonToList(userEntityList,UserLoginForm.class); LoginVO loginVO = new LoginVO(); loginVO.setUserLogFormList(userLoginFormList); return ActionResult.success(loginVO); } } } if(StringUtil.isEmpty(loginForm.getPhonePassword())){ throw new LoginException("賬號(hào)密碼錯(cuò)誤"); } } if(StringUtil.isEmpty(loginForm.getPhonePassword())){ entity = userEntityList.get(0); } userInfo = loginService.userInfo(userInfo, entity); // if(StringUtil.isNotEmpty(loginForm.getRoleId())){ // String[] roles = new String[1]; // roles[0] = loginForm.getRoleId(); // userInfo.setRoleIds(roles); // } // // List<RoleLoginVo> roleLoginVoList = new ArrayList<>(); // // if(ArrayUtils.isNotEmpty(userInfo.getRoleIds())){ // if(userInfo.getRoleIds().length > 1){ // for (String roleId : userInfo.getRoleIds()) { // RoleLoginVo roleLoginVo = JsonUtil.getJsonToBean(roleService.getById(roleId),RoleLoginVo.class); // roleLoginVoList.add(roleLoginVo); // } // } // } // if(CollectionUtil.isNotEmpty(roleLoginVoList)){ // LoginVO loginVO = new LoginVO(); // loginVO.setRoleList(roleLoginVoList); // return ActionResult.success(loginVO); // } userInfo.setMybatisTenantId(entity.getTenantId()); ExpertInfoEntity expertInfoEntity = expertInfoService.getOne( new QueryWrapper<ExpertInfoEntity>().lambda().eq(ExpertInfoEntity::getUserId,entity.getId())); if(null != expertInfoEntity){ userInfo.setExpertId(expertInfoEntity.getId()); } //寫入會(huì)話 userProvider.add(userInfo); //驗(yàn)證賬號(hào)密碼 Map<String, String> map = new HashMap<>(16); map.put("account",loginForm.getAccount()); map.put("password",loginForm.getPassword()); map.putAll(parameters); map.put("username", loginForm.getAccount()); OAuth2AccessToken oAuth2AccessToken; try { oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, map).getBody(); } catch (HttpRequestMethodNotSupportedException e) { throw new LoginException("賬號(hào)密碼錯(cuò)誤"); } TenantContextHolder.setTenant(entity.getTenantId()); //登陸日志記錄在JwtTokenEnhancer類中 //獲取主題 LoginVO loginVO = new LoginVO(); loginVO.setToken(oAuth2AccessToken.getTokenType() + " " + oAuth2AccessToken.getValue()); loginVO.setTheme(entity.getTheme() == null ? "classic" : entity.getTheme()); return ActionResult.success(loginVO); } }
3.前端調(diào)用登陸接口
返回token
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Springboot使用influxDB時(shí)序數(shù)據(jù)庫(kù)的實(shí)現(xiàn)
項(xiàng)目中需要存放大量設(shè)備日志,且需要對(duì)其進(jìn)行簡(jiǎn)單的數(shù)據(jù)分析,信息提取工作,所以本文就介紹一下Springboot使用influxDB時(shí)序數(shù)據(jù)庫(kù),感興趣的可以了解一下2021-08-08Java編寫簡(jiǎn)單計(jì)算器的完整實(shí)現(xiàn)過程
這篇文章主要給大家介紹了關(guān)于Java編寫簡(jiǎn)單計(jì)算器的完整實(shí)現(xiàn)過程,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12Java中JMM與volatile關(guān)鍵字的學(xué)習(xí)
這篇文章主要介紹了通過實(shí)例解析JMM和Volatile關(guān)鍵字的學(xué)習(xí),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2021-09-09Java流程控制之循環(huán)結(jié)構(gòu)for,增強(qiáng)for循環(huán)
這篇文章主要介紹了Java流程控制之循環(huán)結(jié)構(gòu)for,增強(qiáng)for循環(huán),for循環(huán)是編程語(yǔ)言中一種循環(huán)語(yǔ)句,而循環(huán)語(yǔ)句由循環(huán)體及循環(huán)的判定條件兩部分組成,其表達(dá)式為:for(單次表達(dá)式;條件表達(dá)式;末尾循環(huán)體){中間循環(huán)體;},下面我們倆看看文章內(nèi)容的詳細(xì)介紹2021-12-12Java之關(guān)于基本數(shù)據(jù)類型和引用數(shù)據(jù)類型的存放位置
這篇文章主要介紹了Java之關(guān)于基本數(shù)據(jù)類型和引用數(shù)據(jù)類型的存放位置,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07SpringBoot整合Quartz及異步調(diào)用的案例
Quartz是一個(gè)完全由java編寫的開源作業(yè)調(diào)度框架、它的簡(jiǎn)單易用受到業(yè)內(nèi)人士的一致好評(píng),這篇文章主要介紹了SpringBoot整合Quartz及異步調(diào)用,需要的朋友可以參考下2023-03-03Java EasyExcel實(shí)現(xiàn)導(dǎo)出多sheet并設(shè)置單元格樣式
EasyExcel是一個(gè)基于Java的、快速、簡(jiǎn)潔、解決大文件內(nèi)存溢出的Excel處理工具,下面我們就來(lái)學(xué)習(xí)一下EasyExcel如何實(shí)現(xiàn)導(dǎo)出多sheet并設(shè)置單元格樣式吧2023-11-11