博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
使用shiro实现多Realm认证——用户名密码、手机短信验证
阅读量:3951 次
发布时间:2019-05-24

本文共 25451 字,大约阅读时间需要 84 分钟。

为了在网欲音乐系统中实现登入功能,在此使用到了shiro这个安全框架,从而使用到了多Realm认证——用户名密码、手机短信验证。特整理这篇文章希望能让后来者少踩一点坑。

本篇文章环境:SpringBoot 2.0 + shiro+ redis

使用阿里云手机号发送短信验证码

效果展示

  • 用户名密码登入

在这里插入图片描述

  • 手机短语登入

在这里插入图片描述

核心代码刨析

1、用户密码登录realm

package com.music.system.shiro.realm;import com.music.model.User;import com.music.service.impl.UserService;import lombok.extern.slf4j.Slf4j;import org.apache.shiro.authc.*;import org.apache.shiro.authz.AuthorizationInfo;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.subject.PrincipalCollection;import org.springframework.beans.factory.annotation.Autowired;/** * 用户密码登录realm */@Slf4jpublic class UserPasswordRealm extends AuthorizingRealm {
@Autowired private UserService userService; @Override public String getName() {
return LoginType.USER_PASSWORD.getType(); } @Override public boolean supports(AuthenticationToken token) {
if (token instanceof UserToken) {
return ((UserToken) token).getLoginType() == LoginType.USER_PASSWORD; } else {
return false; } } @Override public void setAuthorizationCacheName(String authorizationCacheName) {
super.setAuthorizationCacheName(authorizationCacheName); } @Override protected void clearCachedAuthorizationInfo(PrincipalCollection principals) {
super.clearCachedAuthorizationInfo(principals); } /** * 认证信息.(身份验证) : Authentication 是用来验证用户身份 * */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
log.info("---------------- 用户密码登录 ----------------------"); UsernamePasswordToken token = (UsernamePasswordToken) authcToken; String name = token.getUsername(); // 从数据库获取对应用户名密码的用户 User user = userService.findUserByName(name); if (user != null) {
// 用户为禁用状态 if (!user.getShow()) {
throw new DisabledAccountException(); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user, //用户 user.getPassword(), //密码 getName() //realm name ); return authenticationInfo; } throw new UnknownAccountException(); } /** * 授权 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null; }}

2、手机验证码登录realm

package com.music.system.shiro.realm;import com.music.model.User;import com.music.service.impl.UserService;import com.music.utils.JedisClient;import lombok.extern.slf4j.Slf4j;import org.apache.shiro.authc.*;import org.apache.shiro.authz.AuthorizationInfo;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.subject.PrincipalCollection;import org.springframework.beans.factory.annotation.Autowired;import javax.annotation.Resource;/** * 手机验证码登录realm */@Slf4jpublic class UserPhoneRealm extends AuthorizingRealm {
@Autowired private UserService userService; @Resource JedisClient jedisClient; @Override public String getName() {
return LoginType.USER_PHONE.getType(); } @Override public boolean supports(AuthenticationToken token) {
if (token instanceof UserToken) {
return ((UserToken) token).getLoginType() == LoginType.USER_PHONE; } else {
return false; } } @Override public void setAuthorizationCacheName(String authorizationCacheName) {
super.setAuthorizationCacheName(authorizationCacheName); } @Override protected void clearCachedAuthorizationInfo(PrincipalCollection principals) {
super.clearCachedAuthorizationInfo(principals); } /** * 认证信息.(身份验证) : Authentication 是用来验证用户身份 * */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
log.info("---------------- 手机验证码登录 ----------------------"); UserToken token = (UserToken) authcToken; String phone = token.getUsername(); // 手机验证码 String validCode = String.valueOf(token.getPassword()); System.out.println("validCode------>"+validCode); // 这里从redis中获取了验证码为 123456,并对比密码是否正确 String redisCode = jedisClient.get(phone+"code"); System.out.println("redisCode------>"+redisCode); //线上用redisCode取代123456 if(!"123456".equals(validCode)){
log.debug("验证码错误,手机号为:{}", phone); throw new IncorrectCredentialsException(); } User user = userService.findUserByPhone(phone); if(user == null){
throw new UnknownAccountException(); } // 用户为禁用状态 if(!user.getShow()){
throw new DisabledAccountException(); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user, //用户 validCode, //密码 getName() //realm name ); return authenticationInfo; } /** * 授权 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null; }}

ShiroConfig

package com.music.system.config;import com.music.system.shiro.CredentialsMatcher;import com.music.system.shiro.MyModularRealmAuthenticator;import com.music.system.shiro.SessionManager;import com.music.system.shiro.filter.AuthcShiroFilter;import com.music.system.shiro.filter.SessionControlFilter;import com.music.system.shiro.realm.AuthorizationRealm;import com.music.system.shiro.realm.LoginType;import com.music.system.shiro.realm.UserPasswordRealm;import com.music.system.shiro.realm.UserPhoneRealm;import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;import org.apache.shiro.mgt.SecurityManager;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.realm.Realm;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.apache.shiro.web.servlet.SimpleCookie;import org.crazycake.shiro.RedisCacheManager;import org.crazycake.shiro.RedisManager;import org.crazycake.shiro.RedisSessionDAO;import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import javax.servlet.Filter;import java.util.ArrayList;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;@Configurationpublic class ShiroConfig {
@Value("${spring.redis.host}") private String redisHost; @Value("${spring.redis.port}") private int redisPort; @Value("${spring.redis.password}") private String redisPassword; @Value("${spring.redis.database}") private int database; @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 由于已经重写了authc的拦截器,此处设置的loginUrl和unauthorizedUrl已经没有用了 // 没有登陆的用户只能访问登陆页面,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据 //shiroFilterFactoryBean.setLoginUrl("/common/unauth"); // 登录成功后要跳转的链接 //shiroFilterFactoryBean.setSuccessUrl("/auth/index"); // 未授权界面; //shiroFilterFactoryBean.setUnauthorizedUrl("common/unauth"); //自定义拦截器 Map
filtersMap = new LinkedHashMap
(); //自定义authc访问拦截器 filtersMap.put("authc", new AuthcShiroFilter()); //限制同一帐号同时在线的个数。 filtersMap.put("kickout", kickoutSessionControlFilter()); shiroFilterFactoryBean.setFilters(filtersMap); // 权限控制map. Map
filterChainDefinitionMap = new LinkedHashMap
(); // 公共请求 filterChainDefinitionMap.put("/common/**", "anon"); // 静态资源 filterChainDefinitionMap.put("/static/**", "anon"); // 登录方法 filterChainDefinitionMap.put("/admin/*ogin*", "anon"); // 表示可以匿名访问 filterChainDefinitionMap.put("/admin/sentCode", "anon"); // 表示可以匿名访问 filterChainDefinitionMap.put("/admin/query", "anon"); // 表示可以匿名访问 //此处需要添加一个kickout,上面添加的自定义拦截器才能生效 filterChainDefinitionMap.put("/admin/**", "authc,kickout");// 表示需要认证才可以访问 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.) */ @Bean public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setAuthenticator(modularRealmAuthenticator()); List
realms = new ArrayList<>(); // 统一角色权限控制realm realms.add(authorizingRealm()); // 用户密码登录realm realms.add(userPasswordRealm()); // 用户手机号验证码登录realm realms.add(userPhoneRealm()); securityManager.setRealms(realms); // 自定义缓存实现 使用redis securityManager.setCacheManager(cacheManager()); // 自定义session管理 使用redis securityManager.setSessionManager(sessionManager()); return securityManager; } /** * 自定义的Realm管理,主要针对多realm */ @Bean("myModularRealmAuthenticator") public MyModularRealmAuthenticator modularRealmAuthenticator() {
MyModularRealmAuthenticator customizedModularRealmAuthenticator = new MyModularRealmAuthenticator(); // 设置realm判断条件 customizedModularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy()); return customizedModularRealmAuthenticator; } @Bean public AuthorizingRealm authorizingRealm(){
AuthorizationRealm authorizationRealm = new AuthorizationRealm(); authorizationRealm.setName(LoginType.COMMON.getType()); return authorizationRealm; } /** * 密码登录realm * * @return */ @Bean public UserPasswordRealm userPasswordRealm() {
UserPasswordRealm userPasswordRealm = new UserPasswordRealm(); userPasswordRealm.setName(LoginType.USER_PASSWORD.getType()); // 自定义的密码校验器 userPasswordRealm.setCredentialsMatcher(credentialsMatcher()); return userPasswordRealm; } /** * 手机号验证码登录realm * @return */ @Bean public UserPhoneRealm userPhoneRealm(){
UserPhoneRealm userPhoneRealm = new UserPhoneRealm(); userPhoneRealm.setName(LoginType.USER_PHONE.getType()); return userPhoneRealm; } @Bean public CredentialsMatcher credentialsMatcher() {
return new CredentialsMatcher(); } /** * cacheManager 缓存 redis实现 * 使用的是shiro-redis开源插件 * * @return */ public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); //redis中针对不同用户缓存(此处的id需要对应user实体中的id字段,用于唯一标识) redisCacheManager.setPrincipalIdFieldName("id"); redisCacheManager.setKeyPrefix("SPRINGBOOT_CACHE:"); //设置前缀 return redisCacheManager; } /** * RedisSessionDAO shiro sessionDao层的实现 通过redis * 使用的是shiro-redis开源插件 */ @Bean public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); redisSessionDAO.setKeyPrefix("SPRINGBOOT_SESSION:"); return redisSessionDAO; } /** * Session Manager * 使用的是shiro-redis开源插件 */ @Bean public SessionManager sessionManager() {
SimpleCookie simpleCookie = new SimpleCookie("Token"); simpleCookie.setPath("/"); simpleCookie.setHttpOnly(false); SessionManager sessionManager = new SessionManager(); sessionManager.setSessionDAO(redisSessionDAO()); sessionManager.setSessionIdCookieEnabled(false); sessionManager.setSessionIdUrlRewritingEnabled(false); sessionManager.setDeleteInvalidSessions(true); sessionManager.setSessionIdCookie(simpleCookie); return sessionManager; } /** * 配置shiro redisManager * 使用的是shiro-redis开源插件 * * @return */ public RedisManager redisManager() {
RedisManager redisManager = new RedisManager(); redisManager.setHost(redisHost); redisManager.setPort(redisPort); redisManager.setTimeout(1800); //设置过期时间 redisManager.setPassword(redisPassword); redisManager.setDatabase(database); return redisManager; } /** * 限制同一账号登录同时登录人数控制 * * @return */ // 这里的@Bean不要启用了,自定义的filter不要交由Spring创建,否则会出现被标记为anon的url仍然会执行该自定义过滤器。 updated 2020.06.05 //@Bean public SessionControlFilter kickoutSessionControlFilter() {
SessionControlFilter kickoutSessionControlFilter = new SessionControlFilter(); kickoutSessionControlFilter.setCache(cacheManager()); kickoutSessionControlFilter.setSessionManager(sessionManager()); kickoutSessionControlFilter.setKickoutAfter(false); kickoutSessionControlFilter.setMaxSession(1); kickoutSessionControlFilter.setKickoutUrl("/common/kickout"); return kickoutSessionControlFilter; } /*** * 授权所用配置 * * @return */ @Bean public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } /*** * 使授权注解起作用不如不想配置可以在pom文件中加入 *
*
org.springframework.boot
*
spring-boot-starter-aop
*
* @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * Shiro生命周期处理器 * 此方法需要用static作为修饰词,否则无法通过@Value()注解的方式获取配置文件的值 * */ @Bean public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor(); }}

LoginType文件

package com.music.system.shiro.realm;public enum LoginType {
/** * 通用 */ COMMON("common_realm"), /** * 用户密码登录 */ USER_PASSWORD("user_password_realm"), /** * 手机验证码登录 */ USER_PHONE("user_phone_realm"), /** * 第三方登录(微信登录) */ WECHAT_LOGIN("wechat_login_realm"); private String type; private LoginType(String type) {
this.type = type; } public String getType() {
return type; } @Override public String toString() {
return this.type.toString(); }}

LoginController

/*  Created by IntelliJ IDEA.  User: Kalvin  Date: 2020/10/26  Time: 12:29*/package com.music.controller;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import com.music.model.User;import com.music.service.IUserService;import com.music.service.impl.UserService;import com.music.system.enums.ResultStatusCode;import com.music.system.shiro.realm.LoginType;import com.music.system.shiro.realm.UserToken;import com.music.system.vo.Result;import com.music.utils.JedisClient;import com.music.utils.OptionalLog;import com.music.utils.PhoneCode;import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.IncorrectCredentialsException;import org.apache.shiro.authc.LockedAccountException;import org.apache.shiro.authc.UnknownAccountException;import org.apache.shiro.subject.Subject;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;import javax.servlet.http.HttpServletRequest;import java.util.Map;import static com.music.system.shiro.realm.LoginType.USER_PASSWORD;import static com.music.system.shiro.realm.LoginType.USER_PHONE;@CrossOrigin@RestController@RequestMapping("admin")public class LoginController {
@Autowired private UserService userService; @Resource JedisClient jedisClient; /** * 用户密码登录 */ @RequestMapping("/login") public Result login(HttpServletRequest request){
String loginName = request.getParameter("name"); String password = request.getParameter("password"); request.getSession().setAttribute("loginName",loginName); System.out.println("我是登录页的"+request.getSession().getAttribute("loginName")); System.out.println(loginName+"---->"+password); UserToken token = new UserToken(LoginType.USER_PASSWORD, loginName, password); return shiroLogin(token,LoginType.USER_PASSWORD); } /** * 用户点击获取验证码请求 * */ @RequestMapping("/sentCode") public Result sentCode(@RequestBody Map map){
String phone = (String)map.get("phone"); System.out.println("phone----->"+phone); // PhoneCode.getPhonemsg(phone,jedisClient); //上线打开注释 return new Result(1); } /** * 手机验证码登录 */ @RequestMapping("/loginByPhone") public Result loginByPhone(@RequestBody Map map){
String phone = (String)map.get("phone"); String code = (String)map.get("code"); System.out.println(phone+"---->"+code); UserToken token = new UserToken(LoginType.USER_PHONE, phone, code); System.out.println("token---->"+token); return shiroLogin(token,LoginType.USER_PHONE); } @OptionalLog(modules="操作日志", methods="查询操作日志") @RequestMapping("/query") public void listLogInfo(){
System.out.println("我进入来了。。。。"); } public Result shiroLogin(UserToken token,LoginType loginType){
User user = null; String userName = null; String phone = null; try {
//登录不在该处处理,交由shiro处理 Subject subject = SecurityUtils.getSubject(); System.out.println("subject-------->"+subject); if(LoginType.USER_PASSWORD.equals(loginType)){
userName = token.getUsername(); user = userService.findUserByName(userName); }else if(LoginType.USER_PHONE.equals(loginType)){
phone = token.getUsername(); user = userService.findUserByPhone(phone); } System.out.println(phone+"================="+userName); //出现异常 subject.login(token); if (subject.isAuthenticated()&&user!=null) {
JSON json = new JSONObject(); ((JSONObject) json).put("token", subject.getSession().getId()); ((JSONObject) json).put("user",user); return new Result(ResultStatusCode.OK, json); }else{
return new Result(ResultStatusCode.SHIRO_ERROR); } }catch (IncorrectCredentialsException | UnknownAccountException e){
e.printStackTrace(); return new Result(ResultStatusCode.NOT_EXIST_USER_OR_ERROR_PWD); }catch (LockedAccountException e){
e.printStackTrace(); return new Result(ResultStatusCode.USER_FROZEN); }catch (Exception e){
e.printStackTrace(); return new Result(ResultStatusCode.SYSTEM_ERR); } }}

自定义多realm登录策略

package com.music.system.shiro;import com.music.system.shiro.realm.LoginType;import com.music.system.shiro.realm.UserToken;import org.apache.shiro.authc.AuthenticationException;import org.apache.shiro.authc.AuthenticationInfo;import org.apache.shiro.authc.AuthenticationToken;import org.apache.shiro.authc.pam.ModularRealmAuthenticator;import org.apache.shiro.realm.Realm;import java.util.Collection;import java.util.HashMap;/** * 自定义多realm登录策略 */public class MyModularRealmAuthenticator extends ModularRealmAuthenticator {
@Override protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
// 判断getRealms()是否返回为空 assertRealmsConfigured(); // 所有Realm Collection
realms = getRealms(); // 登录类型对应的所有Realm HashMap
realmHashMap = new HashMap<>(realms.size()); for (Realm realm : realms) {
realmHashMap.put(realm.getName(), realm); } UserToken token = (UserToken) authenticationToken; System.out.println("token------>"+token); // 登录类型 LoginType loginType = token.getLoginType(); if (realmHashMap.get(loginType.getType()) != null) {
return doSingleRealmAuthentication(realmHashMap.get(loginType.getType()), token); } else {
return doMultiRealmAuthentication(realms, token); } }}

JedisClient Java操作redis工具(可扩展)

/*  Created by IntelliJ IDEA.  User: Kalvin  Date: 2020/5/13  Time: 14:21*/package com.music.utils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Component;import java.util.Map;import java.util.Set;import java.util.concurrent.TimeUnit;@Componentpublic class JedisClient {
@Autowired private StringRedisTemplate redisTemplate; // Key(键),简单的key-value操作 /** * 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。 * * @param key * @return */ public long ttl(String key) {
return redisTemplate.getExpire(key); } /** * 实现命令:expire 设置过期时间,单位秒 * * @param key * @return */ public void expire(String key, long timeout) {
redisTemplate.expire(key, timeout, TimeUnit.SECONDS); } /** * 实现命令:INCR key,增加key一次 * * @param key * @return */ public long incr(String key, long delta) {
return redisTemplate.opsForValue().increment(key, delta); } /** * 实现命令:KEYS pattern,查找所有符合给定模式 pattern的 key */ public Set
keys(String pattern) {
return redisTemplate.keys(pattern); } /** * 实现命令:DEL key,删除一个key * * @param key */ public void del(String key) {
redisTemplate.delete(key); } // String(字符串) /** * 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key) * * @param key * @param value */ public void set(String key, String value) {
redisTemplate.opsForValue().set(key, value); } /** * 实现命令:SET key value EX seconds,设置key-value和超时时间(秒) * * @param key * @param value * @param timeout * (以秒为单位) */ public void set(String key, String value, long timeout) {
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS); } /** * 实现命令:GET key,返回 key所关联的字符串值。 * * @param key * @return value */ public String get(String key) {
return (String)redisTemplate.opsForValue().get(key); } // Hash(哈希表) /** * 实现命令:HEXISTS key field,查找哈希表中是否包含指定键值对 key中给定域 field的值 * @param key * @param field * @return */ public Boolean hexists(String key, String field){
return redisTemplate.opsForHash().hasKey(key, field); } /** * 实现命令:HSET key field value,将哈希表 key中的域 field的值设为 value * * @param key * @param field * @param value */ public void hset(String key, String field, Object value) {
redisTemplate.opsForHash().put(key, field, value); } /** * 实现命令:HGET key field,返回哈希表 key中给定域 field的值 * * @param key * @param field * @return */ public String hget(String key, String field) {
return (String) redisTemplate.opsForHash().get(key, field); } /** * 实现命令:HDEL key field [field ...],删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。 * * @param key * @param fields */ public void hdel(String key, Object... fields) {
redisTemplate.opsForHash().delete(key, fields); } /** * 实现命令:HGETALL key,返回哈希表 key中,所有的域和值。 * * @param key * @return */ public Map
hgetall(String key) {
return redisTemplate.opsForHash().entries(key); } // List(列表) /** * 实现命令:LPUSH key value,将一个值 value插入到列表 key的表头 * * @param key * @param value * @return 执行 LPUSH命令后,列表的长度。 */ public long lpush(String key, String value) {
return redisTemplate.opsForList().leftPush(key, value); } /** * 实现命令:LPOP key,移除并返回列表 key的头元素。 * * @param key * @return 列表key的头元素。 */ public String lpop(String key) {
return (String)redisTemplate.opsForList().leftPop(key); } /** * 实现命令:RPUSH key value,将一个值 value插入到列表 key的表尾(最右边)。 * * @param key * @param value * @return 执行 LPUSH命令后,列表的长度。 */ public long rpush(String key, String value) {
return redisTemplate.opsForList().rightPush(key, value); }}

有关使用阿里云手机发送短信验证码,下篇文章详细介绍。这里只是分享核心代码,帮助理解记忆。

有兴趣的小伙伴可以关注微信公众号:幽灵邀请函 回复:网欲音乐 获取更多项目资料及完整项目,一起学习可以加QQ:2817670312

转载地址:http://inuzi.baihongyu.com/

你可能感兴趣的文章
SQL Server循环执行动态SQL语句.
查看>>
ubuntu10.4网卡名由eth0改为eth4,导致获得不了IP地址.解决方法.
查看>>
CheckPoint关键词做字段名使用.
查看>>
Qt QSplitte分割器使用(用户手动改变窗口大小)
查看>>
Qt动态加载动态库
查看>>
java8新特性
查看>>
git clone时RPC failed; curl 18 transfer closed with outstanding read data remaining
查看>>
Java8内存模型—永久代(PermGen)和元空间(Metaspace)
查看>>
maven中jar、war、pom的区别
查看>>
maven之pom.xml配置文件详解
查看>>
java基础学习之抽象类与接口的区别
查看>>
java基础学习之包、类、方法、属性、常量的命名规则
查看>>
java基础知识学习之匿名内部类
查看>>
SSM框架和SSH框架的区别
查看>>
Elasticsearch-基础介绍及索引原理分析
查看>>
过滤敏感词算法
查看>>
linux学习之shell脚本if判断参数-n,-d,-f等
查看>>
linux学习之windos文件在linux里面乱码解决
查看>>
idea快捷键
查看>>
linux学习之shell遍历数组
查看>>