技术标签: Spring Security
配套资料,免费下载
链接:https://pan.baidu.com/s/1EINPwP4or0Nuj8BOEPsIyw
提取码:kbue
复制这段内容后打开百度网盘手机App,操作更方便哦
SSO术语介绍:
单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的一种分布式登录方式。
SSO实现流程:
首先,我们要明确,在分布式项目中,每台服务器都有各自独立的session,而这些session之间是无法直接共享资源的,所以,session通常不能被作为单点登录的技术方案。最合理的单点登录方案流程如下图所示:
单点登录的实现分两大环节:
从分布式认证流程中,我们不难发现,这中间起最关键作用的就是token,token的安全与否,直接关系到系统的健壮性,这里我们选择使用JWT来实现token的生成和校验。JWT,全称JSON Web Token,官网地址:https://jwt.io,是一款出色的分布式身份校验方案,可以生成token,也可以解析检验token。
JWT生成的token由三部分组成:
那么,一个完整的jwt字符串到底长成什么样呢?
JWT生成的token的安全性分析:
从JWT生成的token组成上来看,要想避免token被伪造,主要就得看签名部分了,而签名部分又有三部分组成,其中头部和载荷的base64编码,几乎是透明的,毫无安全性可言,那么最终守护token安全的重担就落在了加入的盐上面了,试想,如果生成token所用的盐与解析token时加入的盐是一样的。岂不是类似于中国人民银行把人民币防伪技术公开了?大家可以用这个盐来解析token,就能用来伪造token。 这时,我们就需要对盐采用非对称加密的方式进行加密,以达到生成token与校验token方所用的盐不一致的安全效果!
注意:加盐的意思就是让味道改变,也就是让通过加盐来提高token的复杂度,让token更加安全,这个盐你可以任意指定,全凭自己和项目需求。
RSA术语介绍:
1976年,两位美国计算机学家Whitfield Diffie 和 Martin Hellman,提出了一种崭新构思,可以在不直接传递密钥的情况下,完成解密。这被称为"Diffie-Hellman密钥交换算法"。这个算法启发了其他科学家。人们认识到,加密和解密可以使用不同的规则,只要这两种规则之间存在某种对应关系即可,这样就避免了直接传递密钥。这种新的加密模式被称为"非对称加密算法"。
RSA是1977年由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的,当时他们三人都在麻省理工学院工作,RSA就是他们三人姓氏开头字母拼在一起组成的 。从那时直到现在,RSA算法一直是最广为使用的"非对称加密算法"。毫不夸张地说,只要有计算机网络的地方,就有RSA算法。这种算法非常可靠,密钥越长,它就越难破解。根据已经披露的文献,目前被破解的最长RSA密钥是768个二进制位。也就是说,长度超过768位的密钥,还无法破解(至少没人公开宣布)。因此可以认为,1024位的RSA密钥基本安全,2048位的密钥极其安全。
RSA使用流程:
基本使用流程,同时生成两把密钥:私钥和公钥,私钥保存起来,公钥可以下发给信任客户端
因此,我们认证服务一般存放私钥和公钥,而资源服务一般存放公钥。私钥负责加密,公钥负责解密。
为了方便大家能够快速进行学习,我已经提前搭建好了一个基本工程,工程代码在配套资料中,名称叫单点登录基础代码
,这只是一个普通的Spring Boot
工程,该工程由四个子模块组成,一个认证服务模块,一个通用工具模块,一个订单资源模块,一个产品资源模块,我已经帮大家创建好了基本的包结构,并在父工程中对Spring Boot
的版本进行了管理,在接下来的代码展示环节中,我并不会展示全部代码,我只展示核心代码,完整代码我会给出。
DROP DATABASE IF EXISTS `test`;
CREATE DATABASE `test`;
USE `test`;
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);
注意:一定一定要注意,我这里的数据库是mysql5.5,并且我的端口为3309而不是3306,接下来的
application.yaml
中对数据源的配置一定要注意修改。
注意:本章节所有操作均在
sso-common
中进行。
pom.xml
<dependencies>
<!--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>
</dependencies>
com.caochenlei.domain.Payload
/**
* 为了方便后期获取token中的用户信息,将token中载荷部分单独封装成一个对象
*
* @author CaoChenLei
*/
@Data
public class Payload<T> implements Serializable {
private String id;
private T userInfo;
private Date expiration;
}
com.caochenlei.domain.Result
/**
* 统一处理返回结果
*
* @author CaoChenLei
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result implements Serializable {
private Integer code;
private String msg;
private Object data;
}
com.caochenlei.utils.JsonUtils
/**
* 对Jackson中的方法进行了简单封装
*
* @author CaoChenLei
*/
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;
}
}
}
com.caochenlei.utils.JwtUtils
/**
* 生成token以及校验token相关方法
*
* @author CaoChenLei
*/
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))
.setId(createJTI())
.setExpiration(DateTime.now().plusMinutes(expire).toDate())
.signWith(privateKey, SignatureAlgorithm.RS256)
.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());
claims.setUserInfo(JsonUtils.toBean(body.get(JWT_PAYLOAD_USER_KEY).toString(), userType));
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;
}
}
com.caochenlei.utils.RsaUtils
/**
* 对Rsa操作进行了简单封装
*
* @author CaoChenLei
*/
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);
}
}
com.caochenlei.utils.RsaUtilsTest
public class RsaUtilsTest {
private String publicFile = "D:\\auth_key\\rsa_key.pub";
private String privateFile = "D:\\auth_key\\rsa_key";
private String secret = "CaoChenLeiSecret";
@Test
public void generateKey() throws Exception {
RsaUtils.generateKey(publicFile, privateFile, secret, 2048);
}
}
注意:本章节所有操作均在
sso-auth-server
中进行。
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<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.caochenlei</groupId>
<artifactId>sso-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
application.yaml
一定一定要注意:
server:
port: 9001
servlet:
application-display-name: sso-auth-server
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3309/test
username: root
password: root
mybatis:
type-aliases-package: com.caochenlei.domain
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.caochenlei: debug
#自定义属性,配置私钥路径
rsa:
key:
privateKeyPath: D:\auth_key\rsa_key
com.caochenlei.prop.RsaKeyProperties
@Data
@ConfigurationProperties(prefix = "rsa.key", ignoreInvalidFields = true)
public class RsaKeyProperties {
private String publicKeyPath;
private String privateKeyPath;
private PublicKey publicKey;
private PrivateKey privateKey;
/**
* 该方法用于初始化公钥和私钥的内容
*/
@PostConstruct
public void loadRsaKey() throws Exception {
if (publicKeyPath != null) {
publicKey = RsaUtils.getPublicKey(publicKeyPath);
}
if (privateKeyPath != null) {
privateKey = RsaUtils.getPrivateKey(privateKeyPath);
}
}
}
com.caochenlei.AuthServerApplication
@SpringBootApplication
@EnableConfigurationProperties(RsaKeyProperties.class)
public class AuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServerApplication.class, args);
}
}
com.caochenlei.utils.RequestUtils
/**
* 请求工具类
*
* @author CaoChenLei
*/
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;
}
}
}
com.caochenlei.utils.ResponseUtils
/**
* 响应工具类
*
* @author CaoChenLei
*/
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);
PrintWriter out = response.getWriter();
out.write(JsonUtils.toString(new Result(status, msg, null)));
out.flush();
out.close();
} catch (Exception e) {
logger.error("响应出错:" + msg, e);
}
}
}
com.caochenlei.domain.SysUser
@Data
public class SysUser implements UserDetails {
private Integer id;
private String username;
private String password;
private Integer status;
private List<SysRole> sysRoles;
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return sysRoles;
}
/**
* 是否账号已过期
*/
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return status != 1;
}
/**
* 是否账号已被锁
*/
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return status != 2;
}
/**
* 是否凭证已过期
*/
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return status != 3;
}
/**
* 是否账号已禁用
*/
@JsonIgnore
@Override
public boolean isEnabled() {
return status != 4;
}
}
com.caochenlei.domain.SysRole
@Data
public class SysRole implements GrantedAuthority {
private Integer id;
private String name;
private String desc;
@JsonIgnore
@Override
public String getAuthority() {
return name;
}
}
com.caochenlei.mapper.SysUserMapper
@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.caochenlei.mapper.SysRoleMapper.findByUid")
)
})
SysUser findByUsername(String username);
}
com.caochenlei.mapper.SysRoleMapper
@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);
}
com.caochenlei.service.SysUserDetailsService
public interface SysUserDetailsService extends UserDetailsService {
}
com.caochenlei.service.impl.SysUserDetailsServiceImpl
@Service
@Transactional
public class SysUserDetailsServiceImpl implements SysUserDetailsService {
@Autowired(required = false)
private SysUserMapper sysUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名去数据库中查询指定用户,这就要保证数据库中的用户的名称必须唯一,否则将会报错
SysUser sysUser = sysUserMapper.findByUsername(username);
//如果没有查询到这个用户,说明数据库中不存在此用户,认证失败,此时需要抛出用户账户不存在
if (sysUser == null) {
throw new UsernameNotFoundException("user not exist.");
}
return sysUser;
}
}
com.caochenlei.filter.JwtAuthenticationFilter
/**
* 认证过滤器
*
* @author CaoChenLei
*/
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private RsaKeyProperties prop;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
this.authenticationManager = authenticationManager;
this.prop = prop;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
SysUser sysUser = RequestUtils.read(request, SysUser.class);
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);
}
/**
* 认证成功所执行的方法
*/
@Override
protected 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, "用户认证通过!");
}
/**
* 认证失败所执行的方法
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
//清理上下文
SecurityContextHolder.clearContext();
System.out.println(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, "用户账户已被锁!");
}
}
}
com.caochenlei.config.WebSecurityConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SysUserDetailsService sysUserDetailsService;
@Autowired
private RsaKeyProperties prop;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
public AuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(sysUserDetailsService);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
return daoAuthenticationProvider;
}
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProvider());
}
public 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));
}
}
注意:本章节所有操作均在
sso-source-order
中进行。
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<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.caochenlei</groupId>
<artifactId>sso-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
application.yaml
一定一定要注意:
server:
port: 9002
servlet:
application-display-name: sso-source-order
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3309/test
username: root
password: root
mybatis:
type-aliases-package: com.caochenlei.domain
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.caochenlei: debug
#自定义属性,配置公钥路径
rsa:
key:
publicKeyPath: D:\auth_key\rsa_key.pub
com.caochenlei.prop.RsaKeyProperties
@Data
@ConfigurationProperties(prefix = "rsa.key", ignoreInvalidFields = true)
public class RsaKeyProperties {
private String publicKeyPath;
private String privateKeyPath;
private PublicKey publicKey;
private PrivateKey privateKey;
/**
* 该方法用于初始化公钥和私钥的内容
*/
@PostConstruct
public void loadRsaKey() throws Exception {
if (publicKeyPath != null) {
publicKey = RsaUtils.getPublicKey(publicKeyPath);
}
if (privateKeyPath != null) {
privateKey = RsaUtils.getPrivateKey(privateKeyPath);
}
}
}
com.caochenlei.SourceOrderApplication(这里注意启动类名称)
@SpringBootApplication
@EnableConfigurationProperties(RsaKeyProperties.class)
public class SourceOrderApplication {
public static void main(String[] args) {
SpringApplication.run(SourceOrderApplication.class, args);
}
}
com.caochenlei.utils.RequestUtils
/**
* 请求工具类
*
* @author CaoChenLei
*/
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;
}
}
}
com.caochenlei.utils.ResponseUtils
/**
* 响应工具类
*
* @author CaoChenLei
*/
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);
PrintWriter out = response.getWriter();
out.write(JsonUtils.toString(new Result(status, msg, null)));
out.flush();
out.close();
} catch (Exception e) {
logger.error("响应出错:" + msg, e);
}
}
}
com.caochenlei.filter.JwtVerificationFilter
/**
* 验证过滤器
*
* @author CaoChenLei
*/
public class JwtVerificationFilter extends BasicAuthenticationFilter {
private RsaKeyProperties prop;
public JwtVerificationFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
super(authenticationManager);
this.prop = prop;
}
@Override
public 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的格式正确,则先要获取到token
String 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, "请您重新登录!");
}
}
}
com.caochenlei.config.WebSecurityConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private RsaKeyProperties prop;
public 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));
}
}
com.caochenlei.exception.GlobalExceptionHandler
@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);
}
}
com.caochenlei.controller.OrderController
@RestController
@RequestMapping("/order")
public class OrderController {
@Secured({
"ROLE_ADMIN","ROLE_ORDER"})
@RequestMapping("/info")
public String info() {
return "Order Controller ...";
}
}
注意:本章节所有操作均在
sso-source-product
中进行。
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<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.caochenlei</groupId>
<artifactId>sso-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
application.yaml
一定一定要注意:
server:
port: 9003
servlet:
application-display-name: sso-source-product
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3309/test
username: root
password: root
mybatis:
type-aliases-package: com.caochenlei.domain
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.caochenlei: debug
#自定义属性,配置公钥路径
rsa:
key:
publicKeyPath: D:\auth_key\rsa_key.pub
com.caochenlei.prop.RsaKeyProperties
@Data
@ConfigurationProperties(prefix = "rsa.key", ignoreInvalidFields = true)
public class RsaKeyProperties {
private String publicKeyPath;
private String privateKeyPath;
private PublicKey publicKey;
private PrivateKey privateKey;
/**
* 该方法用于初始化公钥和私钥的内容
*/
@PostConstruct
public void loadRsaKey() throws Exception {
if (publicKeyPath != null) {
publicKey = RsaUtils.getPublicKey(publicKeyPath);
}
if (privateKeyPath != null) {
privateKey = RsaUtils.getPrivateKey(privateKeyPath);
}
}
}
com.caochenlei.SourceProductApplication(这里注意启动类名称)
@SpringBootApplication
@EnableConfigurationProperties(RsaKeyProperties.class)
public class SourceProductApplication {
public static void main(String[] args) {
SpringApplication.run(SourceProductApplication.class, args);
}
}
com.caochenlei.utils.RequestUtils
/**
* 请求工具类
*
* @author CaoChenLei
*/
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;
}
}
}
com.caochenlei.utils.ResponseUtils
/**
* 响应工具类
*
* @author CaoChenLei
*/
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);
PrintWriter out = response.getWriter();
out.write(JsonUtils.toString(new Result(status, msg, null)));
out.flush();
out.close();
} catch (Exception e) {
logger.error("响应出错:" + msg, e);
}
}
}
com.caochenlei.filter.JwtVerificationFilter
/**
* 验证过滤器
*
* @author CaoChenLei
*/
public class JwtVerificationFilter extends BasicAuthenticationFilter {
private RsaKeyProperties prop;
public JwtVerificationFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
super(authenticationManager);
this.prop = prop;
}
@Override
public 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的格式正确,则先要获取到token
String 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, "请您重新登录!");
}
}
}
com.caochenlei.config.WebSecurityConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private RsaKeyProperties prop;
public 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));
}
}
com.caochenlei.exception.GlobalExceptionHandler
@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);
}
}
com.caochenlei.controller.OrderController
@RestController
@RequestMapping("/product")
public class ProductController {
@Secured({
"ROLE_ADMIN", "ROLE_PRODUCT"})
@RequestMapping("/info")
public String info() {
return "Productr Controller ...";
}
}
张三:
李四:
王五:
赵六:
小七:
老八:
密码错误:
文章浏览阅读1.2k次,点赞35次,收藏18次。AutowiredPostConstruct 注释用于在依赖关系注入完成之后需要执行的方法上,以执行任何初始化。此方法必须在将类放入服务之前调用。支持依赖关系注入的所有类都必须支持此注释。即使类没有请求注入任何资源,用 PostConstruct 注释的方法也必须被调用。只有一个方法可以用此注释进行注释。_springboot2.7获取bean
文章浏览阅读2.1k次。理论介绍 节点定义package logistic;public class Instance { public int label; public double[] x; public Instance(){} public Instance(int label,double[] x){ this.label = label; th_logisticregression java
文章浏览阅读981次,点赞21次,收藏18次。本书是获得了很多读者好评的Linux经典畅销书**《Linux从入门到精通》的第2版**。下面我们来进行文件的恢复,执行下文中的lsof命令,在其返回结果中我们可以看到test-recovery.txt (deleted)被删除了,但是其存在一个进程tail使用它,tail进程的进程编号是1535。我们看到文件名为3的文件,就是我们刚刚“误删除”的文件,所以我们使用下面的cp命令把它恢复回去。命令进入该进程的文件目录下,1535是tail进程的进程id,这个文件目录里包含了若干该进程正在打开使用的文件。
文章浏览阅读10w+次,点赞12次,收藏72次。RTMP(Real Time Messaging Protocol)实时消息传输协议是Adobe公司提出得一种媒体流传输协议,其提供了一个双向得通道消息服务,意图在通信端之间传递带有时间信息得视频、音频和数据消息流,其通过对不同类型得消息分配不同得优先级,进而在网传能力限制下确定各种消息得传输次序。_rtmp
文章浏览阅读64次。2017年12月的计算机等级考试将要来临!出国留学网为考生们整理了2017年12月计算机一级MSOffice考试习题,希望能帮到大家,想了解更多计算机等级考试消息,请关注我们,我们会第一时间更新。2017年12月计算机一级MSOffice考试习题(二)一、单选题1). 计算机最主要的工作特点是( )。A.存储程序与自动控制B.高速度与高精度C.可靠性与可用性D.有记忆能力正确答案:A答案解析:计算...
文章浏览阅读356次。在学MYSQL的时候刚刚好看到了这个提权,很久之前用过别人现成的,但是一直时间没去细想, 这次就自己复现学习下。 0x00 UDF 什么是UDF? UDF (user defined function),即用户自定义函数。是通过添加新函数,对MySQL的功能进行扩充,就像使..._the provided input file '/usr/share/metasploit-framework/data/exploits/mysql
文章浏览阅读3.1w次,点赞71次,收藏485次。webService一 WebService概述1.1 WebService是什么WebService是一种跨编程语言和跨操作系统平台的远程调用技术。Web service是一个平台独立的,低耦合的,自包含的、基于可编程的web的应用程序,可使用开放的XML(标准通用标记语言下的一个子集)标准...
文章浏览阅读1w次。前言照例给出官网:Retrofit官网其实大家学习的时候,完全可以按照官网Introduction,自己写一个例子来运行。但是百密一疏,官网可能忘记添加了一句非常重要的话,导致你可能出现如下错误:Could not locate ResponseBody converter错误信息:Caused by: java.lang.IllegalArgumentException: Could not l_已添加addconverterfactory 但是 could not locate responsebody converter
文章浏览阅读1k次。一套键鼠控制Windows+Linux——Synergy在Windows10和Ubuntu18.04共控的实践Synergy简介准备工作(重要)Windows服务端配置Ubuntu客户端配置配置开机启动Synergy简介Synergy能够通过IP地址实现一套键鼠对多系统、多终端进行控制,免去了对不同终端操作时频繁切换键鼠的麻烦,可跨平台使用,拥有Linux、MacOS、Windows多个版本。Synergy应用分服务端和客户端,服务端即主控端,Synergy会共享连接服务端的键鼠给客户端终端使用。本文_linux 18.04 synergy
文章浏览阅读374次。写demo的时候遇到了很多问题,记录一下。安装nacos1.4.0配置mysql数据库,新建nacos_config数据库,并根据初始化脚本新建表,使配置从数据库读取,可单机模式启动也可以集群模式启动,启动时 ./start.sh -m standaloneapplication.properties 主要是db部分配置## Copyright 1999-2018 Alibaba Group Holding Ltd.## Licensed under the Apache License,_seata1.4.0 +nacos 集成
文章浏览阅读833次。iperf使用方法详解 iperf3是一款带宽测试工具,它支持调节各种参数,比如通信协议,数据包个数,发送持续时间,测试完会报告网络带宽,丢包率和其他参数。 安装 sudo apt-get install iperf3 iPerf3常用的参数: -c :指定客户端模式。例如:iperf3 -c 192.168.1.100。这将使用客户端模式连接到IP地址为192.16..._iperf客户端指定ip地址
文章浏览阅读7.4k次。 写这个函数目的不是为了和C/C++库中的函数在性能和安全性上一比高低,只是为了给那些喜欢探讨函数内部实现的网友,提供一种从浮点性到字符串转换的一种途径。 浮点数是有精度限制的,所以即使我们在使用C/C++中的sprintf或者cout 限制,当然这个精度限制是可以修改的。比方在C++中,我们可以cout.precision(10),不过这样设置的整个输出字符长度为10,而不是特定的小数点后1_c++浮点数 转 字符串 精度损失最小