什么是单点登录?
单点登录,简称SSO,在分布式架构项目中,只需要在一个节点上进行登录验证,就能够实现其它所有节点的访问。
1.创建一个springboot项目,在它基础上new一个module
主项目的依赖:包含了一些配置,可以直接贴贴到主项目的pom.xml里
4.0.0
pom
common_api
org.springframework.boot
spring-boot-starter-parent
2.3.4.RELEASE
com.blb
java0307s4
0.0.1-SNAPSHOT
java0307s4
Demo project for Spring Boot
1.8
Hoxton.SR8
2.2.5.RELEASE
org.springframework.cloud
spring-cloud-dependencies
${spring.cloud-version}
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
${spring-cloud-alibaba.version}
pom
import
org.springframework.boot
spring-boot-starter
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
接着就是用户服务的创建了,我们通常将实体类写到一个新的maven项目中,我们同样new一个module-- Common_api在里面配置
这是我配置的user实体类;我们还需要一个userTokenVO类如下:
user服务的依赖:
4.0.0
com.blb
java0307s4
0.0.1-SNAPSHOT
com.blb.lb
user_service
0.0.1-SNAPSHOT
user_service
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-web
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
mysql
mysql-connector-java
8.0.29
com.baomidou
mybatis-plus-boot-starter
3.0.1
org.springframework.boot
spring-boot-starter-actuator
com.blb
0.0.1-SNAPSHOT
common_api
org.springframework.boot
spring-boot-starter-security
我们通过springboot自带的springSecurity里的UserDetailsServiceImpl 实现登录,直接在service层创建实现类:启动类加上注解:
@MapperScan("com.blb.lb.user_service.mapper") @EnableDiscoveryClient @SpringBootApplication
package com.blb.lb.user_service.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.blb.common.entity.User;
import com.blb.lb.user_service.mapper.UserMapper;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("1111111111111111111");
User user = userMapper.selectOne(new QueryWrapper().lambda().eq(User::getUsername, username));
System.out.println(user);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
return new org.springframework.security.core.userdetails.User(username, user.getPassword()
, AuthorityUtils.commaSeparatedStringToAuthorityList(""));
}
}
配置 LoginSuccessHandler
package com.blb.lb.user_service.config;
import com.blb.common.util.JwtUtil;
import com.blb.common.util.ResponseResult;
import com.blb.common.util.RsaUtil;
import com.blb.lb.user_service.entity.UserTokenVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
User user = (User) authentication.getPrincipal();
String token = JwtUtil.generateToken(user.getUsername(), RsaUtil.privateKey, JwtUtil.EXPIRE_MINUTES);
System.out.println(token);
UserTokenVO userTokenVO = new UserTokenVO(user.getUsername(), token);
ResponseResult.write(response, ResponseResult.ok(userTokenVO));
log.info("user:{} token:{}", user.getUsername(), token);
}
}
配置 SecurityConfig
package com.blb.lb.user_service.config;
import com.blb.common.util.ResponseResult;
import com.blb.common.util.ResponseStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//配置自定义登录逻辑
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置放行url
http.authorizeRequests()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs"
, "/login", "/logout").permitAll()
.anyRequest().authenticated() //配置其它url要验证
.and()
.formLogin() //配置登录相关
.successHandler(loginSuccessHandler) //配置登录成功的处理器
.failureHandler((req, resp, auth) -> { //配置登录失败的处理器
ResponseResult.write(resp, ResponseResult.error(ResponseStatus.LOGIN_ERROR));
})
.and()
.exceptionHandling()
.authenticationEntryPoint((req, resp, auth) -> { //配置拦截未登录请求的处理
ResponseResult.write(resp, ResponseResult.error(ResponseStatus.AUTHENTICATE_ERROR));
})
.and()
.logout()
.logoutSuccessHandler((req, resp, auth) -> { //配置登出处理器
ResponseResult.write(resp, ResponseResult.ok("注销成功"));
})
.clearAuthentication(true) //清除验证缓存
.and()
.csrf()
.disable() //关闭csrf保护
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS); //不使用session
}
}
启动类加注解:
@MapperScan("com.blb.lb.user_service.mapper")
@EnableDiscoveryClient
@SpringBootApplication
然后我们登录需要的一些工具类放在了刚刚的Common_api中依赖:
java0307s4
com.blb
0.0.1-SNAPSHOT
4.0.0
0.0.1-SNAPSHOT
com.blb
common_api
com.baomidou
mybatis-plus-boot-starter
3.0.1
javax.servlet
javax.servlet-api
io.jsonwebtoken
jjwt
0.9.0
joda-time
joda-time
2.9.9
JWT工具类:
package com.blb.common.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.joda.time.DateTime;
import java.security.PrivateKey;
import java.security.PublicKey;
/**
* JWT工具类
*/
public class JwtUtil {
public static final String JWT_KEY_USERNAME = "username";
public static final int EXPIRE_MINUTES = 120;
/**
* 私钥加密token
*/
public static String generateToken(String username, PrivateKey privateKey, int expireMinutes) {
return Jwts.builder()
.claim(JWT_KEY_USERNAME, username)
.setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
}
/**
* 从token解析用户
*
* @param token
* @param publicKey
* @return
* @throws Exception
*/
public static String getUsernameFromToken(String token, PublicKey publicKey) {
Jws claimsJws = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
Claims body = claimsJws.getBody();
String username = (String) body.get(JWT_KEY_USERNAME);
return username;
}
}
RsaUti 工具类:
package com.blb.common.util;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* RSA工具类
*/
public class RsaUtil {
public static final String RSA_SECRET = "blbweb@#$%"; //秘钥
public static final String RSA_PATH = System.getProperty("user.dir")+"/rsa/";//秘钥保存位置
public static final String RSA_PUB_KEY_PATH = RSA_PATH + "pubKey.rsa";//公钥路径
public static final String RSA_PRI_KEY_PATH = RSA_PATH + "priKey.rsa";//私钥路径
public static PublicKey publicKey; //公钥
public static PrivateKey privateKey; //私钥
/**
* 类加载后,生成公钥和私钥文件
*/
static {
try {
File rsa = new File(RSA_PATH);
if (!rsa.exists()) {
rsa.mkdirs();
}
File pubKey = new File(RSA_PUB_KEY_PATH);
File priKey = new File(RSA_PRI_KEY_PATH);
//判断公钥和私钥如果不存在就创建
if (!priKey.exists() || !pubKey.exists()) {
//创建公钥和私钥文件
RsaUtil.generateKey(RSA_PUB_KEY_PATH, RSA_PRI_KEY_PATH, RSA_SECRET);
}
//读取公钥和私钥内容
publicKey = RsaUtil.getPublicKey(RSA_PUB_KEY_PATH);
privateKey = RsaUtil.getPrivateKey(RSA_PRI_KEY_PATH);
} catch (Exception ex) {
ex.printStackTrace();
throw new RuntimeException(ex);
}
}
/**
* 从文件中读取公钥
*
* @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
*/
public static PublicKey getPublicKey(byte[] bytes) throws Exception {
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
}
/**
* 获取密钥
*
* @param bytes 私钥的字节形式
* @return
* @throws Exception
*/
public static PrivateKey getPrivateKey(byte[] bytes) throws Exception {
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePrivate(spec);
}
/**
* 根据密文,生存rsa公钥和私钥,并写入指定文件
*
* @param publicKeyFilename 公钥文件路径
* @param privateKeyFilename 私钥文件路径
* @param secret 生成密钥的密文
* @throws IOException
* @throws NoSuchAlgorithmException
*/
public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
SecureRandom secureRandom = new SecureRandom(secret.getBytes());
keyPairGenerator.initialize(1024, secureRandom);
KeyPair keyPair = keyPairGenerator.genKeyPair();
// 获取公钥并写出
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
writeFile(publicKeyFilename, publicKeyBytes);
// 获取私钥并写出
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
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);
if (!dest.exists()) {
dest.createNewFile();
}
Files.write(dest.toPath(), bytes);
}
}
ResponseResult :
package com.blb.common.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 响应内容
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResponseResult {
private ResponseStatus status;
private T data;
/**
* 正确返回数据
*/
public static ResponseResult ok(T data){
return new ResponseResult(ResponseStatus.OK,data);
}
/**
* 返回错误消息
*/
public static ResponseResult error(ResponseStatus status){
return new ResponseResult<>(status,status.getMessage());
}
/**
* 返回错误消息
*/
public static ResponseResult error(ResponseStatus status,String err){
return new ResponseResult<>(status,err);
}
/**
* 将数据转换为json,发送给前端
* @param resp
* @param result
*/
public static void write(HttpServletResponse resp,ResponseResult result) throws IOException {
resp.setContentType("application/json;charset=UTF-8");
String s = new ObjectMapper().writeValueAsString(result);
PrintWriter writer = resp.getWriter();
writer.write(s);
writer.close();
}
}
ResponseStatus :
package com.blb.common.util;
public enum ResponseStatus {
OK(200,"请求成功"),
INTERNAL_ERROR(500000,"内部错误"),
LOGIN_ERROR(500001,"账号或密码错误"),
BUSINESS_ERROR(500002,"业务错误"),
AUTHORITY_ERROR(500003,"授权错误"),
AUTHENTICATE_ERROR(403,"验证错误,需要登录");
//响应代码
private Integer code;
//响应信息
private String message;
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
ResponseStatus(Integer code, String message) {
this.code = code;
this.message = message;
}
}
在添加完工具类和响应类后;
通常完成 application.properties后直接启动就可以了。还有前端的登录拦截器:
这是在main.js里的 验证token的过滤器
axios.interceptors.request.use(
config => {
let token = localStorage.getItem("token");
console.log("token:" + token);
if (token) {
//把localStorage的token放在Authorization里
config.headers.Authorization = token;
console.log(config.headers.Authorization )
}
return config;
},
function (err) {
console.log("失败信息" + err);
}
);
//错误响应拦截
axios.interceptors.response.use(res => {
console.log('拦截响应');
console.log(res);
if (res.data.status === 'OK') {
return res;
}
if (res.data.data === '验证错误,需要登录') {
console.log('验证错误,需要登录')
// window.location.href = '/'
MessageBox.alert('没有权限,需要登录', '权限错误', {
confirmButtonText: '跳转登录页面',
callback: action => {
window.location.href = '/'
}
})
} else {
Message.error(res.data.data)
}
})
接着是前端的login.vue 主要代码
然后我们new一个springboot项目为gateway网关:依赖;
4.0.0
com.blb
java0307s4
0.0.1-SNAPSHOT
com.blb
gateway_service
0.0.1-SNAPSHOT
gateway_service
Demo project for Spring Boot
1.8
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
com.blb
0.0.1-SNAPSHOT
common_api
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-gateway
org.springframework.cloud
spring-cloud-starter-bootstrap
3.0.1
先配置白名单:
package com.blb.gateway_service.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Data
@Configuration
@ConfigurationProperties(prefix = "user")
public class WhileListConfig {
private List whiteList;
}
接着配置过滤器:AuthenticationFilter
package com.blb.gateway_service.fliter;
import com.blb.common.util.JwtUtil;
import com.blb.common.util.RsaUtil;
import com.blb.gateway_service.config.WhileListConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* 用户验证过滤器
*/
@Slf4j
@Component
public class AuthenticationFilter implements GlobalFilter, Ordered {
@Autowired
private WhileListConfig whiteListConfig;
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获得请求和响应对象
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//对白名单中的地址放行
List whiteList = whiteListConfig.getWhiteList();
for(String str : whiteList){
if(request.getURI().getPath().contains(str)){
log.info("白名单,放行{}",request.getURI().getPath());
return chain.filter(exchange);
}
}
//获得请求头中Authorization token信息
String token = request.getHeaders().getFirst("Authorization");
System.out.println(token);
try{
//解析token
String username = JwtUtil.getUsernameFromToken(token, RsaUtil.publicKey);
log.info("{}解析成功,放行{}",username,request.getURI().getPath());
return chain.filter(exchange);
}catch (Exception ex){
log.error("token解析失败",ex);
//返回验证失败的响应信息
response.setStatusCode(HttpStatus.UNAUTHORIZED);
DataBuffer wrap = response.bufferFactory().wrap("验证错误,需要登录".getBytes());
return response.writeWith(Mono.just(wrap));
}
}
@Override
public int getOrder() {
return 0;
}
}
再在启动类上添加注解:
@EnableDiscoveryClient @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
接着就是服务的resource下的properties配置了。我将它配置到了Nacos上,实现自动刷新;
Nacos是springcloudAlibaba的组件,我们用这个就不用配置注册中心了。
1. 下载Nacos
下载地址https://github.com/alibaba/nacos/releases
2. 复制文件到Linux的/usr/local目录下
cd /usr/local
tar -vxf nacos-server-1.4.0.tar.gz
cd nacos/bin
sh startup.cmd -m standalone
PS:启动文件位于nacos的bin目录下,cmd以管理员身份运行,再进入到nacos所在的bin目录这里以单机模式启动,除此还有集群模式
3. 打开浏览器,输入http://Linux主机IP:8848/nacos/index.html,就可以看到Nacos的登录界面
进入登录界面,账号密码都是nacos
然后进行配置:
注意服务名称要和idea里的一样,以及文件后缀名,服务名-dev.yaml/properties,后面是根据idea里的配置文件后缀名决定。
下面是具体配置:
server:
port: 9000
# 网关配置
spring:
application:
name: gateway-service
cloud:
gateway:
routes: # 路由
- id: order-service-route
uri: lb://order-service # 服务名称
predicates: # 断言
- Path=/order/**,/orders/** # 匹配路径
- id: product-service-route
uri: lb://product-service
predicates:
- Path=/product/**,/products/**
- id: user-service-route
uri: lb://user-service
predicates:
- Path=/login,/logout,/user/**
globalcors:
cors-configurations: # 跨域配置
'[/**]': # 匹配所有路径
allowed-origins: # 允许的域名
- "http://localhost:8080"
allowed-headers: "*" # 允许的请求头
allowed-methods: "*" # 允许的方法
allow-credentials: true # 是否携带cookie
user:
white-list: # 自定义白名单
- /login
- /logout
以及idea里的配置文件;
spring:
application:
name: gateway-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
prefix: gateway-service
file-extension: yaml
profiles:
active: dev
yaml需要注意缩进 不要配置会失效!!
还有user服务的配置:
server.port=8082
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/db4?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
mybatis-plus.type-aliases-package=com.blb.common.entity
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.mapper-locations=classpath:mapper/*.xml
上面的数据库账号密码需要根据自己的来设置,以及实体类的包路径。
接着是user服务idea里的配置:
spring.application.name=user-service
# 注册nacos
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# nacos配置中心地址
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
# 配置文件的前缀
spring.cloud.nacos.config.prefix=user-service
# 后缀
spring.cloud.nacos.config.file-extension=properties
# profile
spring.profiles.active=dev
#开启全部端点
management.endpoints.web.exposure.include=*
其它的服务配置类似,启动网关服务和用户服务后即可实现单点登录;
这是我写单点登录的一点总结,有任何问题希望留言探讨,看到会第一时间回复,谢谢。