①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳✕✓✔✖
主要参考了一个网友的文章,整体写的不错,在读的过程,能获取一些收获。
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 OAuth2Spring Security源码分析十一:Spring Security OAuth2 整合JWTSpring Security源码分析十二:Spring Security OAuth2 基于JWT实现单点登录Spring Security源码分析十三:Spring Security 基于表达式的权限控制Spring Security源码分析十四:Spring Social社交登录的绑定与解绑Spring Security源码分析十五:Spring Security 页面权限控制Spring Security源码分析十六:Spring Security项目实战
这一章节,分析了认证的过程,有以下点可以思考:
通过DaoAuthenticationProvider
源码的分析,可以知道这个类继承了AbstractUserDetailsAuthenticationProvider
,分别实现了两个方法:
UserDetail
additionalAuthenticationChecks
所以可以模拟DaoAuthenticationProvider
实现自己的Provider
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 是一个接口,提供了一个方法
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
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);}
UserDetailsService
接口作为桥梁,是DaoAuthenticationProvier
与特定用户信息来源进行解耦的地方,UserDetailsService
由UserDetails
和UserDetailsManager
所构成;UserDetails
和UserDetailsManager
各司其责,一个是对基本用户信息进行封装,一个是对基本用户信息进行管理;
特别注意
,UserDetailsService
、UserDetails
以及UserDetailsManager
都是可被用户自定义的扩展点
@Beanpublic UserDetailsService userDetailsService() {UserDetails user = User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build();return new InMemoryUserDetailsManager(user);}
也可以这么来写,系统都是支持的。
@Beanpublic InMemoryUserDetailsManager userDetailsService() {UserDetails user = User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build();return new InMemoryUserDetailsManager(user);}
或者通过@Server
自定一个userDetailsService
也是可以的。
UsernamePasswordAuthenticationFilter
AnonymousAuthenticationFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
(未来会被AuthorizationFilter
)UsernamePasswordAuthenticationFilter
类分析,这个类继承了的AbstractAuthenticationProcessingFilter
。分析这个类,可以学习如何自定义filter
。
这个抽象类的主要功能如下:
判断当前请求是否需要处理
判断 filter 是否可以处理当前的请求,如果不可以则放行交给下一个 filter
调用子类的attemptAuthentication
方法进行验证:
调用抽象方法attemptAuthentication
进行验证,该方法由子类UsernamePasswordAuthenticationFilter
实现.
认证成功,处理session
认证成功以后,回调一些与 session
相关的方法;
认证成功,调用successfulAuthentication
方法
Authentication
放置到 SecurityContextHolder
中POST
Authenticaiton
的实现类UsernamePasswordAuthenticationToken
,(UsernamePasswordAuthenticationToken
调用两个参数的构造方法 setAuthenticated(false))AuthenticationManager
的 authenticate
方法进行验证;可参考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和passwordString 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" propertysetDetails(request, authRequest);#4. 调用 AuthenticationManager 进行验证(子类ProviderManager遍历所有的AuthenticationProvider认证)return this.getAuthenticationManager().authenticate(authRequest);}
微信登陆其实很简单简单,就是 auth2 的登陆步骤,先得到一个 code,然后再得到 token。
网上用微信的一个测试账号,是不管用的,老老实实花 300 元认证一个账户吧,不然后报 scope 错误。
微信提供测试号,是不管用的: https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
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
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"}
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"}
使用 127.0.0.1 也可以,但是为了模拟正式的情况,还是用内网穿透吧。请参考frp 的配置文档。
网上有文章利用Spring Social
来实现微信登陆,但是Spring Social
官方不维护了,所以不推荐。
JustAuth
是一个组件,集成了好多种登陆方法。
这里是一个 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'
justauth:enabled: truetype:WECHAT_OPEN:client-id: client-idclient-secret: secretredirect-uri: https://frp.redhtc.com/oauth/wechat_open/callbackcache:type: default
写一个 SpringBoot 的启动 Application 后,添加一个 controller 就可以了。实现了两个主要的函数:
@Slf4j@RestController@RequestMapping("/oauth")public class TestController {private final AuthRequestFactory factory;public TestController(AuthRequestFactory factory){this.factory=factory;}@GetMappingpublic 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
自己的格式,包含了微信的返回结果.
JustAuth 为了简化程序开发,做了以下两个工作:
就是自己写程序实现微信提供的接口。
上面的代码都是使用了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;}}