springboot項(xiàng)目中集成shiro+jwt完整實(shí)例代碼
簡(jiǎn)介
現(xiàn)在主流的安全框架分別為Shiro和Spring Security。關(guān)于兩者之間的優(yōu)缺點(diǎn)不是本文的重點(diǎn),有興趣的可以在網(wǎng)上搜搜,各種文章也都分析的很清楚。那么簡(jiǎn)單來(lái)說(shuō),Shiro是一個(gè)強(qiáng)大易用的Java安全框架,提供了認(rèn)證、授權(quán)、加密和會(huì)話管理等功能。(不一定要建立所謂的五張表,我們要做到控制自如的使用)
目的
通過(guò)集成shiro,jwt我們要實(shí)現(xiàn):用戶登錄的校驗(yàn);登錄成功后返回成功并攜帶具有身份信息的token以便后續(xù)調(diào)用接口的時(shí)候做認(rèn)證;對(duì)項(xiàng)目的接口進(jìn)行權(quán)限的限定等。
需要的jar
本文使用的gradel作為jar包管理工具,maven也是使用相同的jar
//shiro的jar implementation 'org.apache.shiro:shiro-spring:1.7.1' //jwt的jar implementation 'com.auth0:java-jwt:3.15.0' implementation 'com.alibaba:fastjson:1.2.76' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok'
集成過(guò)程
1.配置shiro
@Configuration
public class ShiroConfig {
/*
* 解決spring aop和注解配置一起使用的bug。如果您在使用shiro注解配置的同時(shí),引入了spring
* aop的starter,會(huì)有一個(gè)奇怪的問(wèn)題,導(dǎo)致shiro注解的請(qǐng)求,不能被映射
*/
@Bean
public static DefaultAdvisorAutoProxyCreator creator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
/**
* Enable Shiro AOP annotation support. --<1>
*
* @param securityManager Security Manager
* @return AuthorizationAttributeSourceAdvisor
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* Use for login password matcher --<2>
*
* @return HashedCredentialsMatcher
*/
@Bean("hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
// set name of hash
matcher.setHashAlgorithmName("SHA-256");
// Storage format is hexadecimal
matcher.setStoredCredentialsHexEncoded(true);
return matcher;
}
/**
* Realm for login --<3>
*
* @param matcher password matcher
* @return PasswordRealm
*/
@Bean
public LoginRealm loginRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) {
LoginRealm loginRealm = new LoginRealm(LOGIN);
loginRealm.setCredentialsMatcher(matcher);
return loginRealm;
}
/**
* JwtReal, use for token validation --<4>
*
* @return JwtRealm
*/
@Bean
public JwtRealm jwtRealm() {
return new JwtRealm(JWT);
}
// --<5>
@Bean
public OurModularRealmAuthenticator userModularRealmAuthenticator() {
// rewrite ModularRealmAuthenticator
DataAuthModularRealmAuthenticator modularRealmAuthenticator = new DataAuthModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return modularRealmAuthenticator;
}
// --<6>
@Bean(name = "securityManager")
public SecurityManager securityManager(
@Qualifier("userModularRealmAuthenticator") OurModularRealmAuthenticatormodular,
@Qualifier("jwtRealm") JwtRealm jwtRealm,
@Qualifier("loginRealm") LoginRealm loginRealm
) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// set realm
manager.setAuthenticator(modular);
// set to use own realm
List<Realm> realms = new ArrayList<>();
realms.add(loginRealm);
realms.add(jwtRealm);
manager.setRealms(realms);
// close Shiro's built-in session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
return manager;
}
// --<7>
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filter = new LinkedHashMap<>(1);
filter.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filter);
Map<String, String> filterMap = new HashMap<>();
filterMap.put("/login/**", "anon");
filterMap.put("/v1/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
}- 開(kāi)啟shiro注解支持,具體原理請(qǐng)參考springboot中shiro使用自定義注解屏蔽接口鑒權(quán)實(shí)現(xiàn)
- 配置shiro登錄驗(yàn)證的密碼加密方式:Shiro 提供了用于加密密碼和驗(yàn)證密碼服務(wù)的 CredentialsMatcher 接口,HashedCredentialsMatcher 正是 CredentialsMatcher 的一個(gè)實(shí)現(xiàn)類(lèi)。
- LoginRealm:自定義的Realm,用于處理用戶登錄驗(yàn)證的Realm,在shiro中驗(yàn)證及授權(quán)等信息會(huì)在Realm中配置,詳細(xì)解釋請(qǐng)參考shiro簡(jiǎn)介
- JwtRealm:自定義的Realm,用戶在登錄后訪問(wèn)服務(wù)時(shí)做token的校驗(yàn),用戶權(quán)限的校驗(yàn)等。
- 配置DataAuthModularRealmAuthenticator:是在項(xiàng)目中存在多個(gè)Realm時(shí),根據(jù)項(xiàng)目的認(rèn)證策略可以選擇匹配需要的Realm。
- SecurityManager:Shiro的核心組件,管理著認(rèn)證、授權(quán)、會(huì)話管理等,在這里我把所有的自定義的Realm等資源加入到SecurityManager中
- Shiro的過(guò)濾器:定制項(xiàng)目的path過(guò)濾規(guī)則,并將我們自定義的Filter加入到Shiro中的shiroFilterFactoryBean中
2.創(chuàng)建自定義Realm
2.1 LoginRealm用于處理用戶登錄
public class LoginRealm extends AuthorizingRealm {
public LoginRealm(String name) {
setName(name);
}
// 獲取user相關(guān)信息的service類(lèi)
@Autowired
private UserLoginService userLoginService;
// supports方法必須重寫(xiě),這是shiro處理流程中的一部分,他會(huì)通過(guò)此方法判斷realm是否匹配的正確
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof LoginDataAutoToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
LoginDataAutoToken token = (LoginDataAutoToken) auth;
serviceLog.info(token.getUsername() + "password auth start...");
User user = userLoginService.selectUserByName(token.getUsername());
if (user == null) throw new UnknownAccountException();
Object credentials = user.getPassword();
// save username and role to Attribute
ServletUtils.userNameRoleTo.accept(user.getUserName(), (int) user.getUserType());
return new SimpleAuthenticationInfo(user, credentials, super.getName());
}
}2.2 JwtRealm用于在登錄之后,用戶的token是否正確以及給當(dāng)前用戶授權(quán)等
public class JwtRealm extends AuthorizingRealm {
public JwtRealm(String name) {
setName(name);
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtDataAutoToken;
}
// 給當(dāng)前用戶授權(quán),只有在訪問(wèn)的接口上配置了shiro的權(quán)限相關(guān)的注解的時(shí)候才會(huì)進(jìn)入此方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
UserEnum.Type userEnum = EnumValue.dataValueOf(
UserEnum.Type.class,
ServletUtils.userNameRoleFrom.get().getUserRole()
);
Set<String> roles = new HashSet<>();
roles.add(userEnum.getDesc());
// 授權(quán)角色如果有其他的權(quán)限則都已此類(lèi)的方式授權(quán)
authorizationInfo.setRoles(roles);
return authorizationInfo;
}
// 驗(yàn)證此次request攜帶的token是否正確,如果正確解析當(dāng)前token,并存入上下文中
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
// verify token
String token = (String) auth.getCredentials();
TokenUtils.verify(token);
TupleNameRole tupleNameRole = TokenUtils.tokenDecode(token);
ServletUtils.userNameRoleTo.accept(tupleNameRole.getUsername(), tupleNameRole.getUserRole());
return new SimpleAuthenticationInfo(token, token, ((JwtDataAutoToken) auth).getName());
}
}2.3 OurModularRealmAuthenticator用于匹配的相應(yīng)的Realm
public class DataAuthModularRealmAuthenticator extends ModularRealmAuthenticator {
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
DataAutoToken dataAutoToken = (DataAutoToken) authenticationToken;
Realm realm = getRealm(dataAutoToken);
return doSingleRealmAuthentication(realm, authenticationToken);
}
private Realm getRealm(DataAutoToken dataAutoToken) {
for (Realm realm : getRealms()) {
// 根據(jù)定義的realm的name和dataAutoToken的name匹配相應(yīng)的realm
if (realm.getName().contains(dataAutoToken.getName())) {
return realm;
}
}
return null;
}
}2.4 DataAutoToken及實(shí)現(xiàn)類(lèi)
DataAuthModularRealmAuthenticator的doSingleRealmAuthentication(realm, authenticationToken)做檢驗(yàn)的時(shí)候需要兩個(gè)參數(shù),一個(gè)是Realm另一個(gè)是我們定義的儲(chǔ)存驗(yàn)證信息的AuthenticationToken或者它的實(shí)現(xiàn)類(lèi)。
DataAutoToken:
public interface DataAutoToken {
String getName();
}LoginDataAutoToken :
public class LoginDataAutoToken extends UsernamePasswordToken implements DataAuthToken {
public LoginDataAuthToken(final String username, final String password) {
super(username, password);
}
@Override
public String getName() {
return LOGIN;
}
}JwtDataAutoToken:
public class JwtDataAutoToken implements AuthenticationToken, DataAuthToken {
private final String token;
public JwtDataAuthToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
@Override
public String getName() {
return JWT;
}
}2.5 JwtFilter處理在shiro配置的自定義的Filter
此類(lèi)用于處理不在登錄下必須攜帶發(fā)行的Token訪問(wèn)接口,如果Token存在,則使用shiro subject做token的和訪問(wèn)權(quán)限的校驗(yàn)。
public class JwtFilter extends BasicHttpAuthenticationFilter {
private final BiConsumer<ServletResponse, ErrorMessage> writeResponse = (response, message) ->
Utils.renderString.accept(
(HttpServletResponse) response,
JSON.toJSONString(ResponseResult.fail(message), SerializerFeature.WriteMapNullValue)
);
/**
* @param request ServletRequest
* @param response ServletResponse
* @param mappedValue mappedValue
* @return 是否成功
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//input request to request log file
requestLog.info(
"path:{}, method:{}",
httpServletRequest.getServletPath(),
httpServletRequest.getMethod()
);
String token = httpServletRequest.getHeader(Constant.TOKEN);
if (token != null) {
return executeLogin(request, response);
} else {
writeResponse.accept(response, ErrorMessage.TOKEN_NOT_EXIST);
return false;
}
}
/**
* execute login
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(Constant.TOKEN);
try {
JwtDataAuthToken jwtToken = new JwtDataAuthToken(token);
// validate user permission
getSubject(request, response).login(jwtToken);
return true;
} catch (AuthenticationException e) {
Throwable throwable = e.getCause();
if (throwable instanceof TokenExpiredException) {
writeResponse.accept(response, ErrorMessage.TOKEN_HAS_EXPIRED);
} else {
writeResponse.accept(response, ErrorMessage.TOKEN_INVALID);
}
}
return false;
}
/**
* support across domains
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) 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"));
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}2.6 controller層登錄和其他接口
@RestController
public class AuthController {
@Autowired
private UserService userService;
@PostMapping("/login")
public ResponseResult<String> login(@RequestBody UserReqDto userReqDto) {
userService.login(userLoginReqDto.getUsername(), userReqDto.getPassword());
return ResponseResult.success();
}
// shiro角色注解,admin才可以訪問(wèn)此接口
@RequiresRoles("admin")
@PostMapping("/v1/user")
public ResponseResult<String> addUser(@RequestBody UserAddReqDto userAddReqDto) {
userService.add(userAddReqDto);
return ResponseResult.success();
}
@PostMapping("/v1/token/verify")
public ResponseResult<String> verify() {
return ResponseResult.success(false);
}
@PostMapping("/v1/token/refresh")
public ResponseResult<String> refresh() {
return ResponseResult.success();
}
}2.7 service層
@Service
public class UserServiceImpl implements UserService {
@Override
public void login(String username, String password) {
// Use shiro to verify the username and password
Subject subject = SecurityUtils.getSubject();
LoginDataAutoToken token = new LoginDataAutoToken(username, password);
subject.login(token);
}
@Transactional
@Override
public void add(UserAddReqDto dto) {
User user = getUserByName.apply(dto.getUsername());
if (user != null) {
throw new DataAuthException(ErrorMessage.USER_ALREADY_EXISTS);
} else {
User newUser = new User();
// 設(shè)置user的信息
post(newUser); // insert user to database
}
}2.8 jwt工具類(lèi)
public final class TokenUtils {
private TokenUtils() {
}
/**
* @param username username
* @param role user role
* @return The encrypted token
*/
public static String createToken(String username, int role) {
Date date = new Date(System.currentTimeMillis() + Constant.TOKEN_EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(username);
return JWT.create()
.withClaim(Constant.USER_NAME, username)
.withClaim(Constant.USER_ROLE, role)
.withExpiresAt(date)
.sign(algorithm);
}
/**
* @param username username
* @param role user role
* @return The encrypted token
*/
public static String refreshToken(String username, int role) {
return createToken(username, role);
}
/**
* refresh token and add to header
*/
public static void refreshToken() {
TupleNameRole tupleNameRole = ServletUtils.userNameRoleFrom.get();
ServletUtils.addHeader.accept(
Constant.TOKEN,
createToken(tupleNameRole.getUsername(), tupleNameRole.getUserRole())
);
}
/**
* verify token
*
* @param token jwtToken
*/
public static void verify(String token) {
try {
TupleNameRole tupleNameRole = tokenDecode(token);
Algorithm algorithm = Algorithm.HMAC256(tupleNameRole.getUsername());
JWTVerifier verifier = JWT.require(algorithm)
.withClaim(Constant.USER_NAME, tupleNameRole.getUsername())
.withClaim(Constant.USER_ROLE, tupleNameRole.getUserRole())
.build();
verifier.verify(token);
} catch (JWTVerificationException e) {
serviceLog.error("token verify fail.", e);
throw e;
}
}
/**
* @param token token
* @return user name and role
*/
public static TupleNameRole tokenDecode(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return new TupleNameRole(
jwt.getClaim(Constant.USER_NAME).asString(),
jwt.getClaim(Constant.USER_ROLE).asInt()
);
} catch (JWTDecodeException e) {
serviceLog.error("Token decode happen exception.", e);
throw e;
}
}
}2.9 其他的一些工具類(lèi)
ServletUtils:與spring context中有關(guān)的一些方法
public final class ServletUtils {
private ServletUtils() {
}
private static final int SCOPE = RequestAttributes.SCOPE_REQUEST;
private static final Supplier<ServletRequestAttributes> servletRequestAttributes = () ->
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
private static final Supplier<HttpServletRequest> request = () -> servletRequestAttributes.get().getRequest();
private static final Supplier<HttpServletResponse> response = () -> servletRequestAttributes.get().getResponse();
private static final Consumer<String> saveUsernameToAttribute = (name) ->
servletRequestAttributes.get().setAttribute(Constant.USER_NAME, name, SCOPE);
private static final Supplier<String> usernameFromAttribute = () ->
(String) servletRequestAttributes.get().getAttribute(Constant.USER_NAME, SCOPE);
private static final Consumer<Integer> saveUserRoleToAttribute = (role) ->
servletRequestAttributes.get().setAttribute(Constant.USER_ROLE, role, SCOPE);
private static final Supplier<Integer> userRoleFromAttribute = () ->
(Integer) servletRequestAttributes.get().getAttribute(Constant.USER_ROLE, SCOPE);
/**
* get token form current request
*/
public static Supplier<String> tokenFromRequest = () -> request.get().getHeader(Constant.TOKEN);
/**
* save current user name and role to attribute
*/
public static BiConsumer<String, Integer> userNameRoleTo = (name, role) -> {
saveUsernameToAttribute.accept(name);
saveUserRoleToAttribute.accept(role);
};
/**
* get user name and role from attribute
*/
public static Supplier<TupleNameRole> userNameRoleFrom = () ->
new TupleNameRole(usernameFromAttribute.get(), userRoleFromAttribute.get());
/**
* add message to response header
*/
public static BiConsumer<String, String> addHeader = (key, value) -> response.get().addHeader(key, value);
}Utils:提供與shiro相同的密碼加密方式、獲取uuid、shiro的Filter層出錯(cuò)不能使用全局異常處理時(shí)的返回信息定制等。
public final class Utils {
private Utils() {
}
/**
* use sha256 encrypt
*/
public static Function<String, String> encryptPassword = (password) -> new Sha256Hash(password).toString();
/**
* get uuid
*/
public static Supplier<String> uuid = () -> UUID.randomUUID().toString().replace("-", "");
/**
* writer message to response
*/
public static BiConsumer<HttpServletResponse, String> renderString = (response, body) -> {
response.setStatus(HttpStatus.OK.value());
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=UTF-8");
try (PrintWriter writer = response.getWriter()) {
writer.print(body);
} catch (IOException e) {
serviceLog.error("response error.", e);
}
};
}2.10 返回結(jié)果定義
@Data
public class ResponseResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
private final String code;
@JSONField(ordinal = 1)
private final String msg;
@JSONField(ordinal = 2)
private T data;
private ResponseResult(String code, String msg) {
this.code = code;
this.msg = msg;
log();
}
private static <T> ResponseResult<T> create(String code, String msg) {
return new ResponseResult<>(code, msg);
}
/**
* No data returned successfully
*
* @return ResponseResult<String>
*/
public static <T> ResponseResult<T> success() {
return success(true);
}
/**
* No data returned successfully
*
* @param refreshToken Whether to refresh token
* @return ResponseResult<String>
*/
public static <T> ResponseResult<T> success(boolean refreshToken) {
if (refreshToken) TokenUtils.refreshToken();
return create(ErrorMessage.SUCCESS.code(), ErrorMessage.SUCCESS.msg());
}
public static <T> ResponseResult<T> success(T data) {
return success(data, true);
}
/**
* Data returned successfully
*
* @param data data
* @param <T> T
* @param refreshToken Whether to refresh token
* @return ResponseResult<T>
*/
public static <T> ResponseResult<T> success(T data, boolean refreshToken) {
ResponseResult<T> responseResult = success(refreshToken);
responseResult.setData(data);
return responseResult;
}
/**
* @param e DCException
* @return ResponseResult<String>
*/
public static ResponseResult<String> fail(DataAuthException e) {
return create(e.getCode(), e.getMsg());
}
/**
* @param errorMessage ErrorMessage
* @return ResponseResult<String>
*/
public static ResponseResult<String> fail(ErrorMessage errorMessage) {
return create(errorMessage.code(), errorMessage.msg());
}
/**
* @param errorMessage DCException
* @return ResponseResult<String>
*/
public static ResponseResult<String> fail(ErrorMessage errorMessage, Object[] detailMessage) {
return create(errorMessage.code(), errorMessage.msg() + Arrays.toString(detailMessage));
}
// Output the information returned
private void log() {
requestLog.info("code:{}, msg:{}", this.getCode(), this.getMsg());
}
}到此這篇關(guān)于springboot項(xiàng)目中集成shiro+jwt詳解+完整實(shí)例的文章就介紹到這了,更多相關(guān)springboot 集成shiro jwt內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
@Schedule?如何解決定時(shí)任務(wù)推遲執(zhí)行
這篇文章主要介紹了@Schedule?如何解決定時(shí)任務(wù)推遲執(zhí)行問(wèn)題。具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09
將對(duì)象轉(zhuǎn)化為字符串的java實(shí)例
這篇文章主要介紹了將對(duì)象轉(zhuǎn)化為字符串的java實(shí)例,有需要的朋友可以參考一下2013-12-12
基于Spark實(shí)現(xiàn)隨機(jī)森林代碼
這篇文章主要為大家詳細(xì)介紹了基于Spark實(shí)現(xiàn)隨機(jī)森林代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08
八種Java中的基本數(shù)據(jù)類(lèi)型詳解
在Java編程中,基本數(shù)據(jù)類(lèi)型是必不可少的一部分,對(duì)于初學(xué)者而言,理解這些基本數(shù)據(jù)類(lèi)型是非常重要的,下面我們就來(lái)學(xué)習(xí)一下Java中的八種基本數(shù)據(jù)類(lèi)型,以及它們的使用方法吧2023-08-08
Java簡(jiǎn)單實(shí)現(xiàn)UDP和TCP的示例
下面小編就為大家?guī)?lái)一篇Java簡(jiǎn)單實(shí)現(xiàn)UDP和TCP的示例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-11-11
基于Java代碼實(shí)現(xiàn)數(shù)字在數(shù)組中出現(xiàn)次數(shù)超過(guò)一半
這篇文章主要介紹了基于Java代碼實(shí)現(xiàn)數(shù)字在數(shù)組中出現(xiàn)次數(shù)超過(guò)一半的相關(guān)資料,需要的朋友可以參考下2016-02-02

