【SpringBoot】集成SpringSecurity+JWT实现多服务单点登录,原来这么easy
Spring Boot+Spring Security+JWT实现单点登录
源码
链接:https://pan.baidu.com/s/1EINPwP4or0Nuj8BOEPsIyw
- 提取码:kbue
一.概念
1.1.SSO
介绍:
- 单点登录(SingleSignOn,SSO),当用户在身份
认证服务器
上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统
。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的一种分布式登录方式。
SSO实现流程:
- 在分布式项目中,
每台服务器都有各自独立的session,而这些session之间是无法直接共享资源的
,所以,session通常不能被作为单点登录的技术方案。最合理的单点登录方案流程如下图所示:
单点登录的实现分2部分:
- 用户认证:客户端向认证服务器发起认证请求,认证服务器给客户端返回令牌token, 主要在
认证服务器
中完成,即图中的A系统,注意认证服务器只能有一个
- 身份校验: 客户端携带token去访问其他资源服务器时,在资源服务器中要对token的真伪进行检验,主要在
资源服务器
中完成,即图中的B系统,这里B系统可以有很多个
1.2.JWT
什么是JWT
- 【JavaWeb】关于JWT做认证授权的十万个理由(JSON Web Token)
1.3.RSA
非对称加密算法
- 服务提供方生成两把密钥(公钥和私钥)。私钥隐秘保存,公钥公开,下发给信任客户端
- 调用方获取提供方的公钥,然后用它对信息加密。
- 提供方接收到调用加密后的信息后,用私钥解密。
RSA算法
- 一直是最广为使用的"非对称加密算法"。毫不夸张地说,只要有计算机网络的地方,就有RSA算法。这种算法非常可靠,密钥越长,它就越难破解。根据已经披露的文献,目前被破解的最长RSA密钥是768个二进制位。也就是说,
长度超过768位的密钥,还无法破解(至少没人公开宣布)。因此可以认为,1024位的RSA密钥基本安全,2048位的密钥极其安全。
RSA使用流程:
- 生成两把密钥:私钥和公钥,私钥保存起来,公钥可以下发给信任客户端
- 私钥加密,
持有私钥或公钥才可以解密
- 公钥加密,
持有私钥才可解密
- 私钥加密,
- 因此,认证服务一般存放
私钥和公钥
,而资源服务一般存放公钥
。私钥负责加密,公钥负责解密。
二.思路
1.分析集中式认证流程
- 用户认证:使用
UsernamePasswordAuthenticationFilter
过滤器中attemptAuthentication()
实现认证功能,该过滤器父类中successfulAuthentication()
实现认证成功后的操作。 - 身份校验:使用
BasicAuthenticationFilter
过滤器中doFilterInternal()
验证是否登录,以决定能否进入后续过滤器。
2.分析分布式认证流程
-
用户认证:分布式项目多数是
前后端分离
的架构,需要修改UsernamePasswordAuthenticationFilter
过滤器中attemptAuthentication()
,让其能够接收请求体。另外,默认successfulAuthentication()
在认证通过后,是把用户信息直接放入session
就完事了- 处理方式:修改
successfulAuthentication()
,在认证通过后生成token并返回给用户。
- 处理方式:修改
-
身份校验: 原来
BasicAuthenticationFilter
过滤器中doFilterInternal()
校验用户是否登录,就是看session
中是否有用户信息- 处理方式:校验逻辑修改为,验证用户携带的
token
合法,并解析出用户信息
,交给SpringSecurity,以便于后续的授权功能可以正常使用。
- 处理方式:校验逻辑修改为,验证用户携带的
//Header.Payload.Signature
HMACSHA245(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
三.工程介绍
1.介绍父工程
因为本案例需要创建多个系统,所以我们使用maven聚合工程
来实现,首先创建一个父工程,导入springboot的父依赖即可
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.4.2</version><relativePath/></parent><modelVersion>4.0.0</modelVersion><groupId>com.oyjp</groupId><artifactId>spring-boot-security-sso-parent</artifactId><version>1.0-SNAPSHOT</version><packaging>pom</packaging><description>通用模块</description><modules><module>sso-common</module><!--通用子模块--><module>sso-auth-server</module><!--认证服务子模块--><module>sso-source-product</module><!--产品资源服务子模块--><module>sso-source-order</module><!--订单资源服务子模块--></modules>
该工程由四个子模块组成,一个认证服务模块,一个通用模块,一个订单资源模块,一个产品资源模块
2.导入数据库
DROP DATABASE IF EXISTS `security_test2`;CREATE DATABASE `security_test2`;USE `security_test2`;DROP TABLE IF EXISTS `sys_role`;CREATE TABLE `sys_role` (`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '角色编号',`name` VARCHAR(32) NOT NULL COMMENT '角色名称',`desc` VARCHAR(32) NOT NULL COMMENT '角色描述',PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;INSERT INTO `sys_role`(`id`,`name`,`desc`) VALUES (1,'ROLE_USER','用户权限');
INSERT INTO `sys_role`(`id`,`name`,`desc`) VALUES (2,'ROLE_ADMIN','管理权限');
INSERT INTO `sys_role`(`id`,`name`,`desc`) VALUES (3,'ROLE_PRODUCT','产品权限');
INSERT INTO `sys_role`(`id`,`name`,`desc`) VALUES (4,'ROLE_ORDER','订单权限');DROP TABLE IF EXISTS `sys_user`;CREATE TABLE `sys_user` (`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '用户编号',`username` VARCHAR(32) NOT NULL COMMENT '用户名称',`password` VARCHAR(128) NOT NULL COMMENT '用户密码',`status` INT(1) NOT NULL DEFAULT '1' COMMENT '用户状态(0:关闭、1:开启)',PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;INSERT INTO `sys_user`(`id`,`username`,`password`,`status`) VALUES (1,'zhangsan','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',0);
INSERT INTO `sys_user`(`id`,`username`,`password`,`status`) VALUES (2,'lisi','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',1);
INSERT INTO `sys_user`(`id`,`username`,`password`,`status`) VALUES (3,'wangwu','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',2);
INSERT INTO `sys_user`(`id`,`username`,`password`,`status`) VALUES (4,'zhaoliu','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',3);
INSERT INTO `sys_user`(`id`,`username`,`password`,`status`) VALUES (5,'xiaoqi','$2a$10$M7fmKpMZEkkzrTBiKie.EeAKZhQDrWAltpCA1y/py5AU/8lyiNB8y',4);DROP TABLE IF EXISTS `sys_user_role`;CREATE TABLE `sys_user_role` (`uid` INT(11) NOT NULL COMMENT '用户编号',`rid` INT(11) NOT NULL COMMENT '角色编号',PRIMARY KEY (`uid`,`rid`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (1,1);
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (1,3);
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (2,1);
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (2,4);
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (3,1);
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (3,2);
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (3,3);
INSERT INTO `sys_user_role`(`uid`,`rid`) VALUES (3,4);
四 通用模块
1.导入依赖
<!--JWT--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.2</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.2</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.2</version><scope>runtime</scope></dependency><!--Jackson--><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-core</artifactId><version>2.11.4</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.11.4</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-annotations</artifactId><version>2.11.4</version></dependency><!--JodaTime--><dependency><groupId>joda-time</groupId><artifactId>joda-time</artifactId><version>2.10.9</version></dependency><!--Lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.16</version></dependency><!--日志包--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></dependency><!--测试包--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-core</artifactId></dependency>
2.统一格式
2.1.统一载荷对象
/*** 为了方便后期获取token中的用户信息,将token中载荷部分单独封装成一个对象* @author JianpengOuYang*/
@Data
public class Payload<T> implements Serializable {private String id;private T userInfo;private Date expiration;
}
2.2.统一返回结果
/*** 统一处理返回结果* @author JianpengOuYang*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result implements Serializable {private Integer code;private String msg;private Object data;
}
3.常用工具
3.1.Json工具类
/*** 对Jackson中的方法进行了简单封装* @author JianpengOuYang*/
public class JsonUtils {private static final Logger logger = LoggerFactory.getLogger(JsonUtils.class);private static final ObjectMapper mapper = new ObjectMapper();/*** 将指定对象序列化为一个json字符串** @param obj 指定对象* @return 返回一个json字符串*/public static String toString(Object obj) {if (obj == null) {return null;}if (obj.getClass() == String.class) {return (String) obj;}try {return mapper.writeValueAsString(obj);} catch (JsonProcessingException e) {logger.error("json序列化出错:" + obj, e);return null;}}/*** 将指定json字符串解析为指定类型对象** @param json json字符串* @param tClass 指定类型* @return 返回一个指定类型对象*/public static <T> T toBean(String json, Class<T> tClass) {try {return mapper.readValue(json, tClass);} catch (IOException e) {logger.error("json解析出错:" + json, e);return null;}}/*** 将指定输入流解析为指定类型对象** @param inputStream 输入流对象* @param tClass 指定类型* @return 返回一个指定类型对象*/public static <T> T toBean(InputStream inputStream, Class<T> tClass) {try {return mapper.readValue(inputStream, tClass);} catch (IOException e) {logger.error("json解析出错:" + inputStream, e);return null;}}/*** 将指定json字符串解析为指定类型集合** @param json json字符串* @param eClass 指定元素类型* @return 返回一个指定类型集合*/public static <E> List<E> toList(String json, Class<E> eClass) {try {return mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(List.class, eClass));} catch (IOException e) {logger.error("json解析出错:" + json, e);return null;}}/*** 将指定json字符串解析为指定键值对类型集合** @param json json字符串* @param kClass 指定键类型* @param vClass 指定值类型* @return 返回一个指定键值对类型集合*/public static <K, V> Map<K, V> toMap(String json, Class<K> kClass, Class<V> vClass) {try {return mapper.readValue(json, mapper.getTypeFactory().constructMapType(Map.class, kClass, vClass));} catch (IOException e) {logger.error("json解析出错:" + json, e);return null;}}/*** 将指定json字符串解析为一个复杂类型对象** @param json json字符串* @param type 复杂类型* @return 返回一个复杂类型对象*/public static <T> T nativeRead(String json, TypeReference<T> type) {try {return mapper.readValue(json, type);} catch (IOException e) {logger.error("json解析出错:" + json, e);return null;}}
}
3.2.Jwt工具类
/*** 生成token以及校验token相关方法** @author JianpengOuYang*/
public class JwtUtils {private static final String JWT_PAYLOAD_USER_KEY = "user";private static String createJTI() {return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));}/*** 私钥加密token** @param userInfo 载荷中的数据* @param privateKey 私钥* @param expire 过期时间,单位分钟* @return JWT*/public static String generateTokenExpireInMinutes(Object userInfo, PrivateKey privateKey, int expire) {return Jwts.builder().claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo))//payload.setId(createJTI())//JID.setExpiration(DateTime.now().plusMinutes(expire).toDate())//过期时间.signWith(privateKey, SignatureAlgorithm.RS256)//Signature,使用privateKey作为密钥.compact();}/*** 私钥加密token** @param userInfo 载荷中的数据* @param privateKey 私钥* @param expire 过期时间,单位秒* @return JWT*/public static String generateTokenExpireInSeconds(Object userInfo, PrivateKey privateKey, int expire) {return Jwts.builder().claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo)).setId(createJTI()).setExpiration(DateTime.now().plusSeconds(expire).toDate()).signWith(privateKey, SignatureAlgorithm.RS256).compact();}/*** 公钥解析token** @param token 用户请求中的token* @param publicKey 公钥* @return Jws<Claims>*/private static Jws<Claims> parserToken(String token, PublicKey publicKey) {return Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token);}/*** 获取token中的用户信息** @param token 用户请求中的令牌* @param publicKey 公钥* @return 用户信息*/public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey, Class<T> userType) {Jws<Claims> claimsJws = parserToken(token, publicKey);Claims body = claimsJws.getBody();Payload<T> claims = new Payload<>();claims.setId(body.getId());//JIDclaims.setUserInfo(JsonUtils.toBean(body.get(JWT_PAYLOAD_USER_KEY).toString(), userType));//获取payload中的用户信息claims.setExpiration(body.getExpiration());//获取过期时间return claims;}/*** 获取token中的载荷信息** @param token 用户请求中的令牌* @param publicKey 公钥* @return 用户信息*/public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey) {Jws<Claims> claimsJws = parserToken(token, publicKey);Claims body = claimsJws.getBody();Payload<T> claims = new Payload<>();claims.setId(body.getId());claims.setExpiration(body.getExpiration());return claims;}
}
3.3.Rsa工具类
/*** 对Rsa操作进行了简单封装** @author JianpengOuYang*/
public class RsaUtils {private static final int DEFAULT_KEY_SIZE = 2048;/*** 从文件中读取公钥** @param filename 公钥保存路径,相对于classpath* @return 公钥对象* @throws Exception*/public static PublicKey getPublicKey(String filename) throws Exception {byte[] bytes = readFile(filename);return getPublicKey(bytes);}/*** 从文件中读取密钥** @param filename 私钥保存路径,相对于classpath* @return 私钥对象* @throws Exception*/public static PrivateKey getPrivateKey(String filename) throws Exception {byte[] bytes = readFile(filename);return getPrivateKey(bytes);}/*** 获取公钥** @param bytes 公钥的字节形式* @return* @throws Exception*/private static PublicKey getPublicKey(byte[] bytes) throws Exception {bytes = Base64.getDecoder().decode(bytes);X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);KeyFactory factory = KeyFactory.getInstance("RSA");return factory.generatePublic(spec);}/*** 获取密钥** @param bytes 私钥的字节形式* @return* @throws Exception*/private static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException {bytes = Base64.getDecoder().decode(bytes);PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);KeyFactory factory = KeyFactory.getInstance("RSA");return factory.generatePrivate(spec);}/*** 根据密文,生成rsa公钥和私钥,并写入指定文件** @param publicKeyFilename 公钥文件路径* @param privateKeyFilename 私钥文件路径* @param secret 生成密钥的密文*/public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret, int keySize) throws Exception {KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");SecureRandom secureRandom = new SecureRandom(secret.getBytes());keyPairGenerator.initialize(Math.max(keySize, DEFAULT_KEY_SIZE), secureRandom);KeyPair keyPair = keyPairGenerator.genKeyPair();// 获取公钥并写出byte[] publicKeyBytes = keyPair.getPublic().getEncoded();publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);writeFile(publicKeyFilename, publicKeyBytes);// 获取私钥并写出byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);writeFile(privateKeyFilename, privateKeyBytes);}private static byte[] readFile(String fileName) throws Exception {return Files.readAllBytes(new File(fileName).toPath());}private static void writeFile(String destPath, byte[] bytes) throws IOException {File dest = new File(destPath);File parentFile = dest.getParentFile();if (!parentFile.exists()) {parentFile.mkdirs();}if (!dest.exists()) {dest.createNewFile();}Files.write(dest.toPath(), bytes);}
}
3.4.Response/Request工具类
/*** 请求工具类** @author oyjp*/
public class RequestUtils {private static final Logger logger = LoggerFactory.getLogger(RequestUtils.class);/*** 从请求对象的输入流中获取指定类型对象** @param request 请求对象* @param clazz 指定类型* @return 指定类型对象*/public static <T> T read(HttpServletRequest request, Class<T> clazz) {try {return JsonUtils.toBean(request.getInputStream(), clazz);} catch (Exception e) {logger.error("读取出错:" + clazz, e);return null;}}
}
/*** 响应工具类** @author oyjp*/
public class ResponseUtils {private static final Logger logger = LoggerFactory.getLogger(ResponseUtils.class);/*** 向浏览器响应一个json字符串** @param response 响应对象* @param status 状态码* @param msg 响应信息*/public static void write(HttpServletResponse response, int status, String msg) {try {response.setHeader("Access-Control-Allow-Origin", "*");response.setHeader("Cache-Control", "no-cache");response.setCharacterEncoding("UTF-8");response.setContentType("application/json");response.setStatus(status);byte[] bytes = JsonUtils.toString(new Result(status, msg, null)).getBytes();OutputStream out = response.getOutputStream();out.write(bytes);} catch (Exception e) {logger.error("响应出错:" + msg, e);}}
}
4.生成密钥
- 使用密钥在指定位置生成公钥/私钥文件
public class RsaUtilsTest {private String publicFile = "E:\\auth_key\\rsa_key.pub";private String privateFile = "E:\\auth_key\\rsa_key";private String secret = "JianpengOuYangSecret";@Testpublic void generateKey() throws Exception {RsaUtils.generateKey(publicFile, privateFile, secret, 2048);}
}
五 认证服务
注意:本章节所有操作均在
sso-auth-server
中进行。
1.导入依赖
<dependencies><!--web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--springSecurity--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--mybatis、mysql--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.4</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.49</version></dependency><!--引入通用子模块--><dependency><groupId>com.oyjp</groupId><artifactId>sso-common</artifactId><version>1.0-SNAPSHOT</version></dependency>
</dependencies>
2.创建配置文件
server:port: 9001servlet:application-display-name: sso-auth-serverspring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/security_test2?useSSL=falseusername: rootpassword: rootmybatis:type-aliases-package: com.oyjp.domainconfiguration:map-underscore-to-camel-case: truelogging:level:com.oyjp: debug#自定义属性,配置私钥路径
rsa:key:privateKeyPath: E:\auth_key\rsa_key
3.编写读取公钥的配置类
@Data
@ConfigurationProperties(prefix = "rsa.key", ignoreInvalidFields = true)
public class RsaKeyProperties {private String publicKeyPath;private String privateKeyPath;private PublicKey publicKey;private PrivateKey privateKey;/*** 该方法用于初始化公钥和私钥的内容*/@PostConstructpublic void loadRsaKey() throws Exception {if (publicKeyPath != null) {publicKey = RsaUtils.getPublicKey(publicKeyPath);}if (privateKeyPath != null) {privateKey = RsaUtils.getPrivateKey(privateKeyPath);}}
}
4.编写启动类
@SpringBootApplication
@EnableConfigurationProperties(RsaKeyProperties.class) //启动时加载配置类
public class AuthServerApplication {public static void main(String[] args) {SpringApplication.run(AuthServerApplication.class, args);}
}
5.编写实体类
用户类实现springSecurity的UserDetails 接口
@Data
public class SysUser implements UserDetails {private Integer id;private String username;private String password;private Integer status;private List<SysRole> sysRoles;@JsonIgnore@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return sysRoles;}/*** 是否账号已过期*/@JsonIgnore@Overridepublic boolean isAccountNonExpired() {return status != 1;}/*** 是否账号已被锁*/@JsonIgnore@Overridepublic boolean isAccountNonLocked() {return status != 2;}/*** 是否凭证已过期*/@JsonIgnore@Overridepublic boolean isCredentialsNonExpired() {return status != 3;}/*** 是否账号已禁用*/@JsonIgnore@Overridepublic boolean isEnabled() {return status != 4;}
}
角色类实现springSecurity的GrantedAuthority接口
@Data
public class SysRole implements GrantedAuthority {private Integer id;private String name;private String desc;@JsonIgnore@Overridepublic String getAuthority() {return name;}
}
6.编写映射接口
查用户信息
@Mapper
public interface SysUserMapper {//根据用户名称查询所对应的用户信息@Select("select * from `sys_user` where `username` = #{username}")@Results({//主键字段映射,property代表Java对象属性,column代表数据库字段@Result(property = "id", column = "id", id = true),//普通字段映射,property代表Java对象属性,column代表数据库字段@Result(property = "username", column = "username"),@Result(property = "password", column = "password"),@Result(property = "status", column = "status"),//角色列表映射,根据用户id查询该用户所对应的角色列表sysRoles@Result(property = "sysRoles", column = "id",javaType = List.class,many = @Many(select = "com.oyjp.mapper.SysRoleMapper.findByUid"))})SysUser findByUsername(String username);
}
查角色信息
@Mapper
public interface SysRoleMapper {//根据用户编号查询角色列表@Select("select * from `sys_role` where id in (" +" select rid from `sys_user_role` where uid = #{uid}" +")")List<SysRole> findByUid(Integer uid);
}
7.编写服务接口
实现springSecurity的UserDetailsService 接口,重新loadUserByUsername()
public interface SysUserDetailsService extends UserDetailsService {}
@Service
@Transactional
public class SysUserDetailsServiceImpl implements SysUserDetailsService {@Autowired(required = false)private SysUserMapper sysUserMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//根据用户名去数据库中查询指定用户,这就要保证数据库中的用户的名称必须唯一,否则将会报错SysUser sysUser = sysUserMapper.findByUsername(username);//如果没有查询到这个用户,说明数据库中不存在此用户,认证失败,此时需要抛出用户账户不存在if (sysUser == null) {throw new UsernameNotFoundException("user not exist.");}return sysUser;}
}
8.编写认证过滤器
/*** 认证过滤器**/
@Slf4j
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {private AuthenticationManager authenticationManager;private RsaKeyProperties prop;public JwtAuthenticationFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {this.authenticationManager = authenticationManager;this.prop = prop;}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {SysUser sysUser = RequestUtils.read(request, SysUser.class);assert sysUser != null;String username = sysUser.getUsername();username = username != null ? username : "";String password = sysUser.getPassword();password = password != null ? password : "";UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);return authenticationManager.authenticate(authRequest);}/*** 认证成功所执行的方法*/@Overrideprotected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {SysUser sysUser = new SysUser();sysUser.setUsername(authResult.getName());sysUser.setSysRoles(new ArrayList(authResult.getAuthorities()));String token = JwtUtils.generateTokenExpireInMinutes(sysUser, prop.getPrivateKey(), 24 * 60);response.addHeader("Authorization", "Bearer " + token);ResponseUtils.write(response, HttpServletResponse.SC_OK, "用户认证通过!");}/*** 认证失败所执行的方法*/@Overrideprotected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {//清理上下文SecurityContextHolder.clearContext();log.error("AuthenticationException",failed);//判断异常类if (failed instanceof InternalAuthenticationServiceException) {ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "认证服务不正常!");} else if (failed instanceof UsernameNotFoundException) {ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用户账户不存在!");} else if (failed instanceof BadCredentialsException) {ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用户密码是错的!");} else if (failed instanceof AccountExpiredException) {ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用户账户已过期!");} else if (failed instanceof LockedException) {ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用户账户已被锁!");} else if (failed instanceof CredentialsExpiredException) {ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用户密码已失效!");} else if (failed instanceof DisabledException) {ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用户账户已被锁!");}}
}
9.编写安全配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate SysUserDetailsService sysUserDetailsService;@Autowiredprivate RsaKeyProperties prop;@Beanpublic BCryptPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}public AuthenticationProvider daoAuthenticationProvider() {DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();//指定认证对象的来源daoAuthenticationProvider.setUserDetailsService(sysUserDetailsService);//指定密码编码的来源daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());daoAuthenticationProvider.setHideUserNotFoundExceptions(false);return daoAuthenticationProvider;}@Overridepublic void configure(AuthenticationManagerBuilder auth) throws Exception {auth.authenticationProvider(daoAuthenticationProvider());}@Overridepublic void configure(HttpSecurity http) throws Exception {//禁用csrf保护机制http.csrf().disable();//禁用cors保护机制http.cors().disable();//禁用session会话http.sessionManagement().disable();//禁用form表单登录http.formLogin().disable();//增加自定义认证过滤器(认证服务需要配置)http.addFilter(new JwtAuthenticationFilter(super.authenticationManager(), prop));}
}
六 订单资源
资源服务可以有很多个,这里只拿订单服务为例,记住,资源服务中只能通过公钥验证认证
。不能签发token
!
-
注意:本章节所有操作均在
sso-source-order
中进行。
1.导入依赖
<!--web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--springSecurity--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--mybatis、mysql--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.4</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.49</version></dependency><!--引入通用子模块--><dependency><groupId>com.oyjp</groupId><artifactId>sso-common</artifactId><version>1.0-SNAPSHOT</version></dependency>
2.编写配置文件
server:port: 9002servlet:application-display-name: sso-source-orderspring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/security_test2?useSSL=falseusername: rootpassword: rootmybatis:type-aliases-package: com.oyjp.domainconfiguration:map-underscore-to-camel-case: true
logging:level:com.oyjp: debug#自定义属性,配置公钥路径
rsa:key:publicKeyPath: E:\auth_key\rsa_key.pub
3.编写读取公钥的配置类
@Data
@ConfigurationProperties(prefix = "rsa.key", ignoreInvalidFields = true)
public class RsaKeyProperties {private String publicKeyPath;private String privateKeyPath;private PublicKey publicKey;private PrivateKey privateKey;/*** 该方法用于初始化公钥和私钥的内容*/@PostConstructpublic void loadRsaKey() throws Exception {if (publicKeyPath != null) {publicKey = RsaUtils.getPublicKey(publicKeyPath);}if (privateKeyPath != null) {privateKey = RsaUtils.getPrivateKey(privateKeyPath);}}
}
5.编写验证过滤器
/*** 验证过滤器** @author oyjp*/
public class JwtVerificationFilter extends BasicAuthenticationFilter {private RsaKeyProperties prop;public JwtVerificationFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {super(authenticationManager);this.prop = prop;}@Overridepublic void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {try {String header = request.getHeader("Authorization");if (header == null || !header.startsWith("Bearer ")) {//如果token的格式错误,则提示用户非法登录chain.doFilter(request, response);ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用户非法登录!");} else {//如果token的格式正确,则先要获取到tokenString token = header.replace("Bearer ", "");//使用公钥进行解密然后来验证token是否正确Payload<SysUser> payload = JwtUtils.getInfoFromToken(token, prop.getPublicKey(), SysUser.class);SysUser sysUser = payload.getUserInfo();if (sysUser != null) {UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), null, sysUser.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authResult);chain.doFilter(request, response);} else {ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "用户验证失败!");}}} catch (ExpiredJwtException e) {ResponseUtils.write(response, HttpServletResponse.SC_FORBIDDEN, "请您重新登录!");}}
}
6.编写安全配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate RsaKeyProperties prop;@Overridepublic void configure(HttpSecurity http) throws Exception {//禁用csrf保护机制http.csrf().disable();//禁用cors保护机制http.cors().disable();//禁用session会话http.sessionManagement().disable();//禁用form表单登录http.formLogin().disable();//增加自定义验证过滤器(资源服务需要配置)http.addFilter(new JwtVerificationFilter(super.authenticationManager(), prop));}
}
7.全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(AccessDeniedException.class)public Result accessDeniedException() {return new Result(403, "用户权限不足!", null);}@ExceptionHandler(RuntimeException.class)public Result serverException() {return new Result(500, "服务出现异常!", null);}
}
8.订单资源控制器
@RestController
@RequestMapping("/order")
public class OrderController {@Secured({"ROLE_ADMIN","ROLE_ORDER"})@RequestMapping("/info")public String info() {return "Order Controller ...";}
}
9.启动类
@SpringBootApplication
@EnableConfigurationProperties(RsaKeyProperties.class)
public class SourceOrderApplication {public static void main(String[] args) {SpringApplication.run(SourceOrderApplication.class, args);}
}
七.产品资源
直接复制订单服务,目录名称改为sso-source-product
,然后修改yml配置文件、controller、启动类
1.修改yml配置文件
- 改一下application-display-name、port 即可
server:port: 9003servlet:application-display-name: sso-source-productspring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/security_test2?useSSL=falseusername: rootpassword: rootmybatis:type-aliases-package: com.oyjp.domainconfiguration:map-underscore-to-camel-case: true
logging:level:com.oyjp: debug#自定义属性,配置公钥路径
rsa:key:publicKeyPath: E:\auth_key\rsa_key.pub
2.产品资源控制器
- 编写产品的controller逻辑
@RestController
@RequestMapping("/product")
public class ProductController {@Secured({"ROLE_ADMIN", "ROLE_PRODUCT"})@RequestMapping("/info")public String info() {return "Productr Controller ...";}
}
3.启动类
@SpringBootApplication
@EnableConfigurationProperties(RsaKeyProperties.class)
public class SourceProductApplication {public static void main(String[] args) {SpringApplication.run(SourceProductApplication.class, args);}
}
八 终极测试
1.认证服务测试
2.订单资源测试
3.产品资源测试
4.用户状态测试
张三
李四
王五
赵六
小七
老八
密码错误:
相关文章:
![](https://img-blog.csdnimg.cn/4be6fb06abe64422a5419fa3e53c29f2.png)
【SpringBoot】集成SpringSecurity+JWT实现多服务单点登录,原来这么easy
Spring BootSpring SecurityJWT实现单点登录 源码 链接:https://pan.baidu.com/s/1EINPwP4or0Nuj8BOEPsIyw 提取码:kbue 一.概念 1.1.SSO 介绍: 单点登录(SingleSignOn,SSO),当用户在身份认证服务器上登录一次以…...
![](https://img-blog.csdnimg.cn/img_convert/558d0390c3acf8ecd1353a1b4fc5858f.png)
手把手教你使用PLSQL远程连接Oracle数据库【内网穿透】
文章目录 前言1. 数据库搭建2. 内网穿透2.1 安装cpolar内网穿透2.2 创建隧道映射 3. 公网远程访问4. 配置固定TCP端口地址4.1 保留一个固定的公网TCP端口地址4.2 配置固定公网TCP端口地址4.3 测试使用固定TCP端口地址远程Oracle 前言 Oracle,是甲骨文公司的一款关系…...
![](https://img-blog.csdnimg.cn/802238ce98674192891c71cf5d10fddb.png)
浅谈Deep Learning 与 Machine Learning 与Artificial Intelligence
文章目录 三者的联系与区别 三者的联系与区别 “Deep Learning is a kind of Machine Learning, and Machine Learning is a kind of Artificial Intelligence.” 人工智能(AI),机器学习(Machine Learning,简称ML&am…...
![](https://img-blog.csdnimg.cn/img_convert/3080c9674680fd1e7ec0895b49e89126.png)
和 Node.js 说拜拜,Deno零配置解决方案
不知道大家注意没有,在我们启动各种类型的 Node repo 时,root 目录很快就会被配置文件塞满。例如,在最新版本的 Next.js 中,我们就有 next.config.js、eslintrc.json、tsconfig.json 和 package.json。而在样式那边,还…...
![](https://img-blog.csdnimg.cn/img_convert/dd7aef911a756a7820a4b459efea4be9.png)
AxureRP制作静态站点发布互联网,实现公网访问【内网穿透】
AxureRP制作静态站点发布互联网,内网穿透实现公网访问 文章目录 AxureRP制作静态站点发布互联网,内网穿透实现公网访问前言1.在AxureRP中生成HTML文件2.配置IIS服务3.添加防火墙安全策略4.使用cpolar内网穿透实现公网访问4.1 登录cpolar web ui管理界面4…...
![](https://img-blog.csdnimg.cn/img_convert/bbc95a39f4befa023915b9cb4c6467fe.jpeg)
【好文推荐】openGauss 5.0.0 数据库安全——全密态探究
前言 写此文章的目的,主要是验证: openGauss 5.0.0 数据库能够实现哪种加密方式的全密态全密态数据库的特点 一、全密态介绍 全密态数据库意在解决数据全生命周期的隐私保护问题,使得系统无论在何种业务场景和环境下,数据在传…...
![](https://img-blog.csdnimg.cn/8826fdcb48bf4a2d8709e6e006cd8657.png)
堆的介绍与堆的实现和调整
个人主页:Lei宝啊 愿所有美好如期而遇 目录 堆的介绍: 关于堆的实现及相关的其他问题: 堆的初始化: 堆的销毁: 插入建堆: 堆向上调整: 交换两个节点的值: 堆向下调整&a…...
![](https://img-blog.csdnimg.cn/f9d254ad15d847f5b5125849e683d6c9.png)
【广州华锐互动】马属直肠检查3D虚拟仿真课件
随着科技的发展,医疗行业也在不断地进行创新。其中,广州华锐互动开发的马属直肠检查3D虚拟仿真课件,为医学教育和实践操作带来了新的可能性。它不仅可以帮助医生提高诊断准确率,还可以让医学生在没有真实病人的情况下进行实践操作…...
![](https://img-blog.csdnimg.cn/img_convert/764fcede2cc623b2128ebe3ef35a65f2.png)
Nuxt 菜鸟入门学习笔记:路由
文章目录 路由 Routing页面 Pages导航 Navigation路由参数 Route Parameters路由中间件 Route Middleware路由验证 Route Validation Nuxt 官网地址: https://nuxt.com/ 路由 Routing Nuxt 的一个核心功能是文件系统路由器。pages/目录下的每个 Vue 文件都会创建一…...
![](https://www.ngui.cc/images/no-images.jpg)
C++基本语法和注释
C程序介绍 C 程序可以定义为对象的集合,这些对象通过调用彼此的方法进行交互。现在让我们简要地看一下什么是类、对象,方法、即时变量。 对象 - 对象具有状态和行为。例如:一只狗的状态 - 颜色、名称、品种,行为 - 摇动、叫唤、吃…...
![](https://img-blog.csdnimg.cn/4093b225fe9a420fa0c592ad741a775d.png)
CSRF攻击
防御策略 过滤判断换referer头,添加tocken令牌验证,白名单 CSRF攻击和XSS比较 相同点:都是欺骗用户 不同点: XSS有攻击特征,所有输入点都要考虑代码,单引号过滤 CSRF没有攻击特征,利用的点…...
![](https://img-blog.csdnimg.cn/a240ddd3b2f748f28e22ae53bf55d4de.png)
2023 “华为杯” 中国研究生数学建模竞赛(D题)深度剖析|数学建模完整代码+建模过程全解全析
问题一:区域碳排放量以及经济、人口、能源消费量的现状分析 思路: 定义碳排放量 Prediction 模型: CO2 P * (GDP/P) * (E/GDP) * (CO2/E) 其中: CO2:碳排放量 P:人口数量 GDP/P:人均GDP E/GDP:单位GDP能耗 CO2/E:单位能耗碳排放量 2.收集并统计相关…...
![](https://img-blog.csdnimg.cn/bdf4ba456d63498fa0f07591321226a2.png)
【Proteus仿真】【STM32单片机】基于单片机的智能晾衣架控制系统
文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 系统运行后,LCD1604显示传感器检测的温湿度、光线强度和风速,工作模式,以及相应阈值,系统工作状态等;系统默认为自动模式, 可通过K4…...
![](https://img-blog.csdnimg.cn/5b599c14e6814260b301008b10d08539.png)
C/C++代码静态检测工具PC-Lint常见错误总结
目录 1、PC-Lint 概述 2、PC-lint 常见错误列举 3、PC-Lint报告的语法错误 4、总结 VC常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C软件异常排查从入门到…...
![](https://csdnimg.cn/release/blog_editor_html/release2.3.6/ckeditor/plugins/CsdnLink/icons/icon-default.png?t=N7T8)
概率深度学习建模数据不确定性
https://zhuanlan.zhihu.com/p/568912284理解论文 What uncertainties do we need in Bayesian deep learning for computer vision? (NeurIPS 2017) [1]中的数据不确定性建模,并给出公式推导。论文[1]指出不确定性uncertainty分为随机不确定性(aleator…...
![](https://img-blog.csdnimg.cn/f4acfea6767b4c7e8475fe13f6d63fd7.png)
Jenkins自动化部署前后端分离项目 (svn + Springboot + Vue + maven)有图详解
1. 准备工作 本文的前后端分离项目,技术框架是: Springboot Vue Maven SVN Redis Mysql Nginx JDK 所以首先需要安装以下: 在腾讯云服务器OpenCLoudOS系统中安装jdk(有图详解) 在腾讯云服务器OpenCLoudOS系统…...
![](https://img-blog.csdnimg.cn/0ce38284fa3241af92295092054b33ab.png)
【ELK】日志系统部署
一、ELK日志分析系统 1、ELK的组成 ElasticSearchLogStashKibana ELK基于这三个开源日志的收集、存储、检索和可视化的解决方案;可帮助用户快速定位和分析应用程序的故障,监控应用程序性能和安全,以及提供丰富的数据分析和展示功能。 2、完…...
![](https://img-blog.csdnimg.cn/713de93a61f74b2b813666c3e6c32683.gif)
【算法挨揍日记】day08——30. 串联所有单词的子串、76. 最小覆盖子串
30. 串联所有单词的子串 30. 串联所有单词的子串 题目描述: 给定一个字符串 s 和一个字符串数组 words。 words 中所有字符串 长度相同。 s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。 例如,如果 words ["…...
![](https://img-blog.csdnimg.cn/img_convert/2b6e247434f915e4cfc54f9485a0de0e.png)
SpringCloud Gateway--网关服务基本介绍和基本原理
😀前言 本篇博文是关于SpringCloud Gateway的基本介绍,希望你能够喜欢 🏠个人主页:晨犀主页 🧑个人简介:大家好,我是晨犀,希望我的文章可以帮助到大家,您的满意是我的动力…...
![](https://img-blog.csdnimg.cn/7ed67728a4024652ab01528c2bb029eb.png)
使用Vue-cli构建spa项目及结构解析
一,Vue-cli是什么? 是一个官方发布的Vue脚手架工具,用于快速搭建Vue项目结构,提供了现代前端开发所需要的一些基础功能,例如:Webpack打包、ESLint语法检查、单元测试、自动化部署等等。同时,Vu…...
![](https://img-blog.csdnimg.cn/999212284f2243068cf6273157d2c945.png)
自定义Unity组件——AudioManager(音频管理器)
需求描述 在游戏开发中,音频资源是不可或缺的,通常情况下音频资源随机分布,各个音频的操作和管理都是各自负责,同时对于音频的很多操作逻辑都是大同小异的,这就造成了许多冗余代码的堆叠,除此之外在获取各类…...
![](https://www.ngui.cc/images/no-images.jpg)
leetcode 558 设计内存文件系统
题目 Design an in-memory file system to simulate the following functions: ls: Given a path in string format. If it is a file path, return a list that only contains this files name. If it is a directory path, return the list of file and directory namesin th…...
![](https://img-blog.csdnimg.cn/df0f8b98e8f2402db9150ce443c2a0f5.png)
Haproxy负载均衡群集
HAproxy搭建Web群集一、Web集群调度器1、常见的Web集群调度器2、常用集群调度器的优缺点(LVS ,Nginx,Haproxy)2.1 Nginx2.2 LVS2.3 Haproxy 3、LVS、Nginx、HAproxy的区别 二、Haproxy1、简介2、Haproxy应用分析3、HAProxy的主要特性4、Haproxy调度算法(…...
![](https://www.ngui.cc/images/no-images.jpg)
什么是面包屑导航?
面包屑导航(Breadcrumb Navigation)这个概念来自童话故事“汉赛尔和格莱特”,当汉赛尔和格莱特穿过森林时,不小心迷路了,但是他们发现沿途走过的地方都撒下了面包屑,让这些面包屑来帮助他们找到回家的路。 在网站应用中࿰…...
![](https://img-blog.csdnimg.cn/4d5f8b2a044a4e8d9f10a2619b0d0732.png)
VS2019创建GIt仓库时剔除文件或目录
假设本地有解决方案“SomeSolution” 1、首先”团队资源管理器“-“创建Git存储库”,选择“仅限本地”、“创建” VS会在解决方案目录下自动生成.gitattributes、.gitignore 2、编辑gitignore,直接拖到VS里或者用记事本打开。添加要剔除的文件或文件夹…...
![](https://img-blog.csdnimg.cn/08513f325abd489ea832ee272e6174d7.png)
计算机等级考试—信息安全三级真题六
目录 一、单选题 二、填空题 三、综合题 一、单选题...
![](https://www.ngui.cc/images/no-images.jpg)
vue循环滚动字幕
在Vue.js中创建一个循环滚动字幕的效果通常需要使用一些CSS和JavaScript来实现。以下是一个简单的示例,展示如何使用Vue.js创建一个循环滚动字幕的效果: 首先,在HTML中创建一个Vue实例,并添加一个包含滚动字幕的容器元素ÿ…...
![](https://img-blog.csdnimg.cn/f16529809b544e619efad382ae515adc.gif)
扩展pytest接口自动化框架-MS数据解析功能
【软件测试行业现状】2023年了你还敢学软件测试?未来已寄..测试人该何去何从?【自动化测试、测试开发、性能测试】 开篇 MeterSphere的数据源通过html页面上传后,需要将请求方式进行拆分。 get接口的参数,常以params的方式进行传…...
![](https://img-blog.csdnimg.cn/74cbd8308b9d4026a0d772c50279e70d.png)
docker容器安装MongoDB数据库
一:MongoDB数据库 1.1 简介 MongoDB是一个开源、高性能、无模式的文档型数据库,当初的设计就是用于简化开发和方便扩展,是NoSQL数据库产品中的一种。是最 像关系型数据库(MySQL)的非关系型数据库。 它支持的数据结构…...
![](https://img-blog.csdnimg.cn/607103b7cd9e4acb8f13ddb4d14d4342.png)
Python机器学习实战-特征重要性分析方法(3):迭代删除法:Leave-one-out(附源码和实现效果)
实现功能 迭代地每次删除一个特征并评估准确性 实现代码 from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score impo…...
![](https://images0.cnblogs.com/blog/56191/201502/091702505731075.jpg)
兰州网站建设/推广平台哪儿有怎么做
临江仙.吴忠漫步 纵目水乡多少埠?引来商贾如云。黄河造就万家春。 古城连广厦,绿树抱街心。 在此唐宗施大略,丝绸驼队铃频。清真美食诱嘉宾。 人勤千业旺,当数穆斯林! 注:吴忠,市名,…...
![](/images/no-images.jpg)
php做网站架构图/国外服务器免费ip地址
Description 青子的生日快到了,快斗计划着带青子去游乐场玩。游乐场有好多游戏项目,每个游戏项目都有自己的开始和结束时间。同一时间只能玩一个游戏,前一个游戏结束后可以马上开始下一个游戏。为了让青子过一个愉快的生日,快斗决…...
![](https://miro.medium.com/max/9999/0*wvRGepVyFVNyd-Nt.png)
万户做网站好不好/网站设计的流程
kotlin jsonWelcome to a brand new tutorial for Easy Android Programming. In the tutorial, we are going to learn about the basic usage of retrofit in kotlin for fetching JSON from a remote server.欢迎使用全新的Easy Android编程教程。 在本教程中,我…...
![](https://img-blog.csdnimg.cn/img_convert/387922ae426e80da253b434d955620aa.png)
wordpress怎么转换为静态链接/太原搜索引擎优化
本系列教程汇总: 买了域名一定需要备案吗?什么情况下不需要备案?如何购买阿里云服务器(图文教程)如何购买阿里云香港服务器(图文教程)如何购买阿里云学生服务器(图文教程) 阿里云是国内第一大云服务器厂商,所以往往我会推荐公司客…...
![](https://img-blog.csdnimg.cn/20190317214843182.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dpbnRlcnNoaWk=,size_16,color_FFFFFF,t_70)
web网站开发学习/外贸推广渠道有哪些
操作系统概述 1.操作系统做了什么 我们编写这样一条C语言代码 #include<stdio.h>int main() {puts("hello world");return 0; }1.用户告诉操作系统执行此程序2.操作系统接收到指令,先去磁盘上找到此程序的相关信息,然后检查其类型是否为可执行文件;紧接着通…...
![](/images/no-images.jpg)
ngix安装wordpress的伪静态/seo短期课程
你好,欢迎来到第 29 课时,从这一课时开始我们进入“Flink 实时统计 PV、UV”项目的学习。本课时先介绍实时统计项目的背景、架构设计和技术选型。 背景 PV(Page View,网站的浏览量)即页面的浏览次数,一般用来衡量网站用户访问的网页数量。我们可以简单地认为,一个用户…...