小伙伴们,你们好呀!我是老寇!废话不多说,跟我一起学习单点登录SSO
1.运行效果图(b站-地址)
2. 老寇云SSO架构
3.老寇云SSO授权模式
4.老寇云SSO流程图(个人理解)
5.老寇云SSO流程说明(个人理解)?
6.核心代码
springsecurity单点登录
1.基础框架:springboot + springcloud
2.认证授权:shiro + jwt (im-sso)、springsecurity + oauth2(security-auth和security-server)
3.缓存:redis
老寇云采用的主要是授权码模式和密码模式
security-auth采用密码模式
security-server采用授权码模式
**第一步:**老寇云加载页发送POST请求并携带client_id、client_secret、grant_type、username、password参数到security-auth获取token
POST https://1.com/auth/laokou-demo/oauth/token
grant_type: password
username: nBG5ht
password: 123
scope: auth
client_id: client_auth
client_secret: secret
**第二步:**security-auth拿到code请求security-server获取access_token
POST http://localhost:9028/laokou-demo/oauth/token
client_id: client_auth
client_secret: secret
redirect_uri: https://1.com/im/loading.html
grant_type: authorization_code
**第三步:**获取token失败,授权码已被使用
**第四步:**响应前端授权码已被使用
**第五步:**发送GET请求并携带参数请求security-server服务
GET http://localhost:9028/laokou-demo/oauth/authorize
response_type: code
client_id: client_auth
redirect_uri: https://1.com/im/loading.html
scope: userInfo
state: 123
**第六步:**如果没有登录,输入账号密码进行登录或登录未过期获取授权码code,并重定向到老寇云加载页
**第七步:**重复第一步的步骤
**第八步:**重复第二步的步骤
**第九步:**授权码可用,获取access_token
第九步1:用拿到的access_token请求security-server的资源服务,获取userKey
GET http://localhost:9028/laokou-demo/userKey
access_token: dsfdsf233
第九步2:security-server响应userKey
**第十步:**用拿到的userKey,获取你所要对接系统的token生成接口,这里是去请求im-sso生成授权码token
**第十一步:**im-sso生成授权码token响应给security-auth
**第十二步:**security-auth将授权码token响应给老寇云加载页
**第十三步:**老寇云加载页拿到token并跳转到老寇云首页
**第十四步:**老寇云首页验证token有效性,token过期又跳转到老寇云加载页
security-auth的yml配置
sso:
token:
client_id: client_auth
client_secret: secret
redirect_uri: https://1.com/im/loading.html
grant_type: authorization_code
security-auth核心配置类
package io.laokou.auth.config;
import io.laokou.auth.token.RenTokenEnhancer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
/**
* TODO
*
* @author Kou Shenhai 2413176044
* @version 1.0
* @date 2021/5/28 0028 下午 4:53
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private WebResponseExceptionTranslator webResponseExceptionTranslator;
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 配置客户端信息
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//in-memory存储
clients.inMemory()
//有一些不需要配置,你可以对照文档去弄
.withClient("client_auth")
//授权类型
.authorizedGrantTypes("password")
.scopes("auth")
.secret("secret")
.autoApprove(true);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST, HttpMethod.DELETE);
//密码模式
endpoints.authenticationManager(authenticationManager);
//令牌增强
endpoints.tokenEnhancer(tokenEnhancer());
//登录或者鉴权失败时的返回信息
endpoints.exceptionTranslator(webResponseExceptionTranslator);
}
@Bean
public TokenEnhancer tokenEnhancer() {
return new RenTokenEnhancer();
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security.allowFormAuthenticationForClients()
.passwordEncoder(passwordEncoder)
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
}
package io.laokou.auth.config;
import io.laokou.auth.filter.ValidateCodeFilter;
import io.laokou.auth.provider.AuthAuthenticationProvider;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Spring Security配置
* @author Kou Shenhai 2413176044
* @version 1.0
* @date 2021/5/28 0028 上午 10:33
*/
@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ValidateCodeFilter validateCodeFilter;
@Autowired
private AuthAuthenticationProvider authAuthenticationProvider;
/**
* 密码模式
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception{
return super.authenticationManager();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//设置自定义认证
auth.authenticationProvider(authAuthenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.requestMatchers().anyRequest()
.and()
.authorizeRequests()
.antMatchers("/oauth/authorize").permitAll();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(HttpMethod.OPTIONS);
}
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
}
security-auth的重写token生成逻辑
package io.laokou.auth.token;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
/**
* TODO
*
* @author Kou Shenhai 2413176044
* @version 1.0
* @date 2021/5/28 0028 下午 5:13
*/
public class RenTokenEnhancer implements TokenEnhancer {
@Autowired
private 自己写的生成token的工具类 工具类实例;
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication auth) {
if (accessToken instanceof DefaultOAuth2AccessToken) {
DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
//添加授权码
String userKey = auth.getUserAuthentication().getPrincipal().toString();
token.setValue(工具类实例.getAuthorize(userKey));
//2秒过后重新认证 -> 本系统只依赖于工具类生成的token,不依赖于springsecurity的token,这么做是方便token过期后,springsecurity这边不能认证的情况(因为springsecurity的token未过期,就不会给你进行重新登录,如果感兴趣可以去读一下源码)
token.setExpiration(DateTime.now().plusSeconds(2).toDate());
return token;
}
return accessToken;
}
}
security-auth拦截器
package io.laokou.auth.filter;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* TODO
* @author Kou Shenhai 2413176044
* @version 1.0
* @date 2021/4/11 0011 下午 2:29
*/
@Component
@AllArgsConstructor
public class ValidateCodeFilter extends OncePerRequestFilter {
private final static AntPathMatcher antPathMatcher = new AntPathMatcher();
private final static String OAUTH_URL = "/oauth/token";
private final static String GRANT_TYPE = "password";
@SneakyThrows
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
if (antPathMatcher.match(request.getServletPath(), OAUTH_URL)
&& request.getMethod().equalsIgnoreCase("POST")
&& GRANT_TYPE.equals(request.getParameter("grant_type"))) {
filterChain.doFilter(request, response);
}
}
}
获取token的工具类
package io.laokou.auth.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.laokou.auth.exception.RenAuthenticationException;
import io.laokou.common.utils.HttpUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* TODO
*
* @author Kou Shenhai 2413176044
* @version 1.0
* @date 2021/4/11 0011 下午 5:16
*/
@Component
@Slf4j
public class AuthUtil {
@Value("${sso.token.client_id}")
private String CLIENT_ID;
@Value("${sso.token.client_secret}")
private String CLIENT_SECRET;
@Value("${sso.token.redirect_uri}")
private String REDIRECT_URI;
@Value("${sso.token.grant_type}")
private String GRANT_TYPE;
private static final String POST_AUTHORIZE_URL = "http://localhost:9028/laokou-demo/oauth/token";
private static final String GET_USER_INFO_URL = "http://localhost:9028/laokou-demo/userKey";
public String getAccessToken(String code) throws IOException {
//将code放入
Map tokenMap = new HashMap<>(5);
tokenMap.put("code",code);
tokenMap.put("client_id",CLIENT_ID);
tokenMap.put("client_secret",CLIENT_SECRET);
tokenMap.put("redirect_uri",REDIRECT_URI);
tokenMap.put("grant_type",GRANT_TYPE);
//根据自己的请求方式,可以自己去写httpclient,你自己去弄
String accessToken = HttpUtil.doPost(POST_AUTHORIZE_URL,tokenMap);
if (StringUtils.isEmpty(accessToken)){
throw new RenAuthenticationException("授权码已过期,请重新获取");
}
JSONObject jsonObject = JSON.parseObject(accessToken);
return jsonObject.getString("access_token");
}
public String getUserKey(String accessToken) throws IOException {
Map userInfoMap = new HashMap<>(1);
userInfoMap.put("access_token",accessToken);
return HttpUtil.doGet(GET_USER_INFO_URL, userInfoMap);
}
}
security-auth认证逻辑实现
package io.laokou.auth.provider;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
/**
* TODO
*
* @author Kou Shenhai 2413176044
* @version 1.0
* @date 2021/4/16 0016 上午 9:45
*/
@Component
@Slf4j
public class AuthAuthenticationProvider implements AuthenticationProvider {
@Autowired
private AuthUtil authUtil;
@SneakyThrows
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String code = authentication.getName();
String password = (String)authentication.getCredentials();
log.info("code:{}",code);
String accessToken = authUtil.getAccessToken(code);
//自己改造获取token的逻辑 -> 懒得写啦,你自己弄
String userKey = authUtil.getUserKey(accessToken);
if (StringUtils.isEmpty(userKey)) {
throw new Exception("账户不存在");
}
UserDetails userDetails = new User( userKey,password,new ArrayList<>());
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails.getUsername(),authentication.getCredentials(),userDetails.getAuthorities());
authenticationToken.setDetails(authentication.getDetails());
return authenticationToken;
}
@Override
public boolean supports(Class> aClass) {
return true;
}
}
security-server配置和上面类似,唯一的就是多了一个资源的服务,这个需要通过用access_token才能访问资源
package io.laokou.security.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
/**
* TODO
*
* @author Kou Shenhai 2413176044
* @version 1.0
* @date 2021/4/16 0016 下午 12:50
*/
public class ResourceServerConfig {
@Configuration()
@EnableResourceServer()
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/userKey")
.and()
.authorizeRequests().antMatchers().authenticated()
.and()
.authorizeRequests().antMatchers("/userKey")
.access("#oauth2.hasScope('userInfo')");
}
}
}
security-server获取的用户唯一标识,通过这个唯一标识获取IM系统的授权码token
package io.laokou.security.controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
/**
* TODO
*
* @author Kou Shenhai 2413176044
* @version 1.0
* @date 2021/4/16 0016 上午 11:19
*/
@RestController
public class ResourceController {
/**
* 唯一标识
* @param principal
* @return
*/
@GetMapping("/userKey")
@CrossOrigin
public String getUserKey(Principal principal) {
return principal.getName();
}
}