其它例子

①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳✕✓✔✖

主要参考了一个网友的文章,整体写的不错,在读的过程,能获取一些收获。

Spring Security源码分析一:Spring Security 认证过程
Spring Security源码分析二:Spring Security 授权过程 -- Spring的授权模式更新了
Spring Security源码分析三:Spring Social 实现QQ社交登录 -- 不推荐
Spring Security源码分析四:Spring Social 实现微信社交登录 -- 不推荐
Spring Security源码分析五:Spring Security 实现短信登录
Spring Security源码分析六:Spring Social 社交登录源码解析
Spring Security源码分析七:Spring Security 记住我
Spring Security源码分析八:Spring Security 退出
Spring Security源码分析九:Spring Security Session管理
Spring Security源码分析十:初识Spring Security OAuth2
Spring Security源码分析十一:Spring Security OAuth2 整合JWT
Spring Security源码分析十二:Spring Security OAuth2 基于JWT实现单点登录
Spring Security源码分析十三:Spring Security 基于表达式的权限控制
Spring Security源码分析十四:Spring Social社交登录的绑定与解绑
Spring Security源码分析十五:Spring Security 页面权限控制
Spring Security源码分析十六:Spring Security项目实战

1. 认证与授权

这一章节,分析了认证的过程,有以下点可以思考:

1.1 自定义 Provider

通过DaoAuthenticationProvider源码的分析,可以知道这个类继承了AbstractUserDetailsAuthenticationProvider,分别实现了两个方法:

  • 获取UserDetail
  • 添加 token 检查additionalAuthenticationChecks

所以可以模拟DaoAuthenticationProvider实现自己的Provider

① AbstractUserDetailsAuthenticationProvider

AbstractUserDetailsAuthenticationProvider主要实现了AuthenticationProvider的接口方法 authenticate 并提供了相关的验证逻辑;

authenticate方法的验证逻辑如下:

获取用户返回UserDetails AbstractUserDetailsAuthenticationProvider定义了一个抽象的方法

protected abstract UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;

三步验证工作

  • preAuthenticationChecks
  • additionalAuthenticationChecks(抽象方法,子类实现)
  • postAuthenticationChecks

返回对象

将已通过验证的用户信息封装成 UsernamePasswordAuthenticationToken 对象并返回;该对象封装了用户的身份信息,以及相应的权限信息,相关源码如下,

protected Authentication createSuccessAuthentication(Object principal,
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}

② UserDetailsService

UserDetailsService 是一个接口,提供了一个方法

public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

UserDetails

public interface UserDetails extends Serializable {
#1.权限集合
Collection<? extends GrantedAuthority> getAuthorities();
#2.密码
String getPassword();
#2.用户民
String getUsername();
#4.用户是否过期
boolean isAccountNonExpired();
#5.是否锁定
boolean isAccountNonLocked();
#6.用户密码是否过期
boolean isCredentialsNonExpired();
#7.账号是否可用(可理解为是否删除)
boolean isEnabled();
}

Spring 为UserDetailsService默认提供了一个实现类 org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl

③ UserDetailsManager

UserDetailsService 只有一个查找用户的函数,略显淡薄,所以 UserDetailsManager,丰富了相关的内容

public interface UserDetailsManager extends UserDetailsService {
void createUser(UserDetails user);
void updateUser(UserDetails user);
void deleteUser(String username);
void changePassword(String oldPassword, String newPassword);
boolean userExists(String username);
}
  • JdbcUserDetailsManager
  • InMemoryUserDetailsManager

1.2 认证总结

UserDetailsService接口作为桥梁,是DaoAuthenticationProvier与特定用户信息来源进行解耦的地方,UserDetailsServiceUserDetailsUserDetailsManager所构成;UserDetailsUserDetailsManager各司其责,一个是对基本用户信息进行封装,一个是对基本用户信息进行管理;

特别注意UserDetailsServiceUserDetails以及UserDetailsManager都是可被用户自定义的扩展点

@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}

也可以这么来写,系统都是支持的。

@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}

或者通过@Server自定一个userDetailsService也是可以的。

1.3 重要的 filter

  • UsernamePasswordAuthenticationFilter
  • AnonymousAuthenticationFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor(未来会被AuthorizationFilter)

1.4 自定义 filter

UsernamePasswordAuthenticationFilter 类分析,这个类继承了的AbstractAuthenticationProcessingFilter。分析这个类,可以学习如何自定义filter

① AbstractAuthenticationProcessingFilter

这个抽象类的主要功能如下:

判断当前请求是否需要处理

判断 filter 是否可以处理当前的请求,如果不可以则放行交给下一个 filter

调用子类的attemptAuthentication方法进行验证:

调用抽象方法attemptAuthentication进行验证,该方法由子类UsernamePasswordAuthenticationFilter实现.

认证成功,处理session

认证成功以后,回调一些与 session 相关的方法;

认证成功,调用successfulAuthentication方法

  • 将当前认证成功的 Authentication 放置到 SecurityContextHolder
  • 调用一些消息处理事件。

② UsernamePasswordAuthenticationFilter

  1. 认证请求的方法必须为POST
  2. 从 request 中获取 username 和 password
  3. 封装Authenticaiton的实现类UsernamePasswordAuthenticationToken,(UsernamePasswordAuthenticationToken调用两个参数的构造方法 setAuthenticated(false))
  4. 调用 AuthenticationManagerauthenticate 方法进行验证;可参考ProviderManager部分;
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
#1.判断请求的方法必须为POST请求
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
#2.从request中获取username和password
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
#2.构建UsernamePasswordAuthenticationToken(两个参数的构造方法setAuthenticated(false)
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
#4. 调用 AuthenticationManager 进行验证(子类ProviderManager遍历所有的AuthenticationProvider认证)
return this.getAuthenticationManager().authenticate(authRequest);
}

2. 微信登陆

微信登陆其实很简单简单,就是 auth2 的登陆步骤,先得到一个 code,然后再得到 token。

2.1 准备工作

2.1.1 申请账户

网上用微信的一个测试账号,是不管用的,老老实实花 300 元认证一个账户吧,不然后报 scope 错误。

微信提供测试号,是不管用的: https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login

① 得到 code

可以参考官方的网址

https://open.weixin.qq.com/connect/qrconnect?response_type=code&appid=wxaf3facb5c9ed1cae&redirect_uri=https://frp.redhtc.com/oauth/wechat_open/callback&scope=snsapi_login&state=state
② 根据 code,得到 token
https://api.weixin.qq.com/sns/oauth2/access_token?appid=wxaf3facb5c9ed1cae&secret=a103a9rc6bacew23798dqdadwe1aad93&code=071LAlFa1dBRfD0wKPGa1Bu6qd2LAlFr&grant_type=authorization_code

返回

{
"access_token": "57_QA4YSL_JlMbaZ2GQm_ktCoYVjOzbUm7fS6DQr1KTTLZUmHkONahMApViMpSj2cZ9ffRSic5RBi6MfslWtHvFUOJTXnXhAj0NIghS2mAb6b8",
"expires_in": 7200,
"refresh_token": "57_m1BlmhGx6wbzkhP8YCtuwLahjczDsQyo6_z8Dw1-lEMsPcWaxQ5maYeQIKncT0z01C4ef0HL02B8W-yZpzZFOJQfpLYKh5FjrVF_e12zSf4",
"openid": "okA4X6r3fiCmkKvOEziLqCVTjXtE",
"scope": "snsapi_login",
"unionid": "oEky758Z_NRIJ8T0_E9De8_PZg2w"
}
③ 获取UnionID
https://api.weixin.qq.com/sns/userinfo?access_token=57_QA4YSL_JlMbaZ2GQm_ktCoYVjOzbUm7fS6DQr1KTTLZUmHkONahMApViMpSj2cZ9ffRSic5RBi6MfslWtHvFUOJTXnXhAj0NIghS2mAb6b8&openid=okA4X6r3fiCmkKvOEziLqCVTjXtE

返回

{
"openid": "okA4X6r3fiCmkKvOEziLqCVTjXtE",
"nickname": "乐逍遥",
"sex": 0,
"language": "",
"city": "",
"province": "",
"country": "",
"headimgurl": "https://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eqvSDpezWYf8uJjwQNFroPgrV2W8SIhBDX9LzTZupgnyU29LXeCZRc6OnRqcmj38W3j3LCJSFKCVg/132",
"privilege": [],
"unionid": "oEky758Z_NRIJ8T0_E9De8_PZg2w"
}

2.1.2 链接到内网

使用 127.0.0.1 也可以,但是为了模拟正式的情况,还是用内网穿透吧。请参考frp 的配置文档

2.1.3 其他

网上有文章利用Spring Social来实现微信登陆,但是Spring Social官方不维护了,所以不推荐。

2.1.4 整合流程

2.2 使用 JustAuth

JustAuth是一个组件,集成了好多种登陆方法。

2.2.1 程序开发

这里是一个 DEMO 程序

① 依赖引用
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.xkcoding.justauth:justauth-spring-boot-starter:1.4.0'
implementation 'org.projectlombok:lombok:1.18.20'
② 配置 application.yml
justauth:
enabled: true
type:
WECHAT_OPEN:
client-id: client-id
client-secret: secret
redirect-uri: https://frp.redhtc.com/oauth/wechat_open/callback
cache:
type: default
③ 写一个 controller

写一个 SpringBoot 的启动 Application 后,添加一个 controller 就可以了。实现了两个主要的函数:

  • 请求 code,会跳转到微信显示二维码的地址,扫描后,跳转回来。
  • 一个回调的函数,用来获取用户信息。
@Slf4j
@RestController
@RequestMapping("/oauth")
public class TestController {
private final AuthRequestFactory factory;
public TestController(AuthRequestFactory factory){
this.factory=factory;
}
@GetMapping
public List<String> list() {
return factory.oauthList();
}
@GetMapping("/login/{type}")
public void login(@PathVariable String type, HttpServletResponse response) throws IOException {
AuthRequest authRequest = factory.get(type);
response.sendRedirect(authRequest.authorize(AuthStateUtils.createState()));
}
@RequestMapping("/{type}/callback")
public AuthResponse login(@PathVariable String type, AuthCallback callback) {
AuthRequest authRequest = factory.get(type);
AuthResponse response = authRequest.login(callback);
return response;
}
}
④ 启动并测试

启动程序后,输入:

https://frp.redhtc.com/oauth/login/wechat_open

注释:frp.redhtc.com 就是本地的测试程序的服务

返回的结果如下:是justAuth自己的格式,包含了微信的返回结果.

2.2.2 关于 JustAuth 思考

JustAuth 为了简化程序开发,做了以下两个工作:

  • 隐藏化了 state。
  • 最重要的简化,以前要分两步得到用户信息,JustAuth 一步就得到了。虽然方便了,但是效率会降低,所以 JustAuth 使用了缓存。

2.3 使用 webclient

就是自己写程序实现微信提供的接口。

上面的代码都是使用了httpClinet作为RestApi访问的接口,但是Spring最新推荐的webclient作为RestApi接口的访问。

下面的代码描述了如何得到 code 以及如何得到 token

@RestController
@RequestMapping("/oauth")
public class TestAuthController {
@Value("${justauth.type.WECHAT_OPEN.client-id}")
private String appid;
@Value("${justauth.type.WECHAT_OPEN.client-secret}")
private String secret;
@Value("${justauth.type.WECHAT_OPEN.redirect-uri}")
private String redirectUri;
@GetMapping("/code")
public void redirectForCode(HttpServletResponse response, HttpServletRequest request) throws IOException {
String state=StringUtils.replace(
java.util.UUID.randomUUID().toString(),"-","");
UriComponents uri= UriComponentsBuilder.newInstance()
.scheme("https").host("open.weixin.qq.com")
.path("/connect/qrconnect")
.queryParam("appid",appid)
.queryParam("redirect_uri",redirectUri)
.queryParam("response_type","code")
.queryParam("scope","snsapi_login")
.queryParam("state",state)
.build();
request.getSession().setAttribute("state",state);
response.sendRedirect(uri.toUriString());
}
@RequestMapping("/{type}/callback")
public Mono<String> login(@PathVariable String type,HttpServletRequest request) {
String state=request.getSession().getAttribute("state").toString();
UriComponents uriComponents= UriComponentsBuilder.fromHttpRequest( new ServletServerHttpRequest(request)).build();
String stateFromServer= uriComponents.getQueryParams().getFirst("state");
if(!state.equals(stateFromServer)){
throw new RuntimeException("state is error");
}
String code =uriComponents.getQueryParams().getFirst("code");
UriComponents uri= UriComponentsBuilder.newInstance()
.scheme("https").host("api.weixin.qq.com")
.path("/sns/oauth2/access_token")
.queryParam("appid",appid)
.queryParam("secret",secret)
.queryParam("grant_type","authorization_code")
.queryParam("scope","snsapi_login")
.queryParam("code",code)
.build();
WebClient webClient=WebClient.create("https://api.weixin.qq.com");
Mono<String> result= webClient.get()
.uri(uri.toUri())
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class)
;
return result;
}
}

2.4 整合现有系统

2.4.1 流程

2.4.2 数据库设计

3. 单点登陆