前言
1.spring-security是spring官方推荐的【认证、授权】框架
2.本文介绍spring-security在【表单认证】、【jwt认证】、【社交登录】3种场景中的运用
3.RBAC权限模块的代码,将以伪代码形式给出
总体介绍
过滤器链
spring-security使用多个Filter来实现:认证、授权、记住我、成功(失败)跳转、跨域访问、防跨站攻击。。。一系列功能。
其中【UsernamePasswordAuthenticationFilter】与【BasicAuthenticationFilter】为核心过滤器(与业务绑定),分别管理【用户名/密码的认证】与【token认证】
认证、授权流程
第一步:认证
根据username查询系统中是否存在该用户
第二步:授权
认证成功后,获取该用户的权限,封装为UserDetail对象(上图对于理解spring security整套认证、授权流程至关重要)
PS:根据此图,调试代码,弄明白spring-security整个认证、授权流程,那么这套框架的核心就理解的差不多了
案例
表单认证
1.流程图
2.实现
核心配置
@Override
protected void configure(HttpSecurity http) throws Exception {
// 允许iframe嵌套,解决:frame because it set 'X-Frame-Options' to 'deny 问题
http.headers().frameOptions().disable();
// 放行所有option请求
http.authorizeRequests()
.requestMatchers(CorsUtils::isPreFlightRequest)
.permitAll();
http.csrf().disable() // 关闭跨站攻击保护,否则无法进行跨域访问
.cors() // 开启跨域支持
.and().formLogin()
.loginPage("/login.html") // 登录页
.loginProcessingUrl("/user/login") // 登录Action提交url
.defaultSuccessUrl("/success.html").permitAll()
.failureUrl("/error.html")
// .failureHandler(failureHandler) // 登录失败自定义处理器(会覆盖failureUrl()的设置)
// .successHandler(successHandler) // 登录成功自定义处理器(会覆盖defaultSuccessUrl()的设置)
.and().logout() // 退出登录
.logoutUrl("/logout") // 退出Action提交url
.addLogoutHandler(definedLogoutHandler) // 退出处理逻辑编写
.and().exceptionHandling().accessDeniedPage("/unauth.html") // 无权限访问时跳转的html
// 放开访问权限的url
.and().authorizeRequests()
.antMatchers("/", "/user/login", "/logout", "/user/logout").permitAll()
.anyRequest().authenticated();
// .and().rememberMe() // 【记住我】功能设置
// .and().sessionManagement() // session管理:session有效期、session个数(可实现互踢)
}
配置的说明,注释已经说的很详细了,可自行阅读
说明:
a. 跨站攻击【.csrf().disable()】必须要关闭,否则跨域配置【.cors()】不起作用
b. 登录action的url【/user/login】与退出登录action的url【/logout】只需要配置路径即可,不需要自己实现,由spring-security框架自行实现
跨域配置
@Bean
CorsFilter corsFilter() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin(CorsConfiguration.ALL);
configuration.addAllowedHeader(CorsConfiguration.ALL);
configuration.addAllowedMethod(CorsConfiguration.ALL);
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return new CorsFilter(source);
}
补充
FormLogin模式虽然依托于session,但是仍然可以实现微服务架构下的单点登录
实现步骤:
a. session需要保存在一个公共可访问区域(数据库 or redis)
b. session登录后,需要扩展session的作用域范围
例如:两个服务:www.baidu.com,map.baidu.com
当在www.baidu.com登录成功后,得到的session的作用域为www.baidu.com,此时我们需要修改session的作用域为:baidu.com。
这样,当我们在登录成功的状态下,再访问map.baidu.com时,由于session的作用域为baidu.com,所以session在map.baidu.com下仍然有效,所以登录状态为【已登录】
jwt认证
1.流程图
BasicLogin模式,采用的是http身份认证,属于最简单的认证、授权功能
2.实现
jwt的认证过程,有两步:【登录-生成token】、【验证token】需要将以上两个步骤,封装为Filter,添加到spring-security整个Filter链中
1.登录-生成token
a. 自定义Filter,用此Filter替换掉UsernamePasswordAuthenticationFilter,这样过滤器链的【用户名-密码认证】,用的就是我们自定义的业务逻辑
b. 继承UsernamePasswordAuthenticationFilter,重写【获取用户传递的信息方法(attemptAuthentication)】、【认证成功方法(successfulAuthentication)】、【认证失败方法(unsuccessfulAuthentication)】,实现自定义认证逻辑
核心代码
获取表单数据:spring-security的认证方法,需要将用户输入封装为:UsernamePasswordAuthenticationToken(用户名,密码,权限)
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
//获取表单提交数据
try {
User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),
user.getPassword()));
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException();
}
}
认证成功
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult) {
//认证成功,得到认证成功之后用户信息
SecurityUser user = (SecurityUser) authResult.getPrincipal();
//根据用户名生成token
String token = tokenManager.generateToken(user);
//返回token
ResponseUtil.out(response, token);
}
认证失败
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) {
ResponseUtil.out(response, "认证失败");
}
2.验证token
a. 自定义Filter,用此Filter替换掉BasicAuthenticationFilter。由于jwt依托于http协议,BasicAuthenticationFilter就是针对http的Filter,所以选择替换掉BasicAUthenticationFilter
b. 继承BasicAuthenticationFilter,重写【过滤器方法(doFilterInternal)】实现自定义业务逻辑
核心代码
认证过滤
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//获取当前认证成功用户权限信息
UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
//判断如果有权限信息,放到权限上下文中
if (authRequest != null) {
SecurityContextHolder.getContext().setAuthentication(authRequest);
}
chain.doFilter(request, response);
}
获取用户认证信息
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
//从header获取token
String token = request.getHeader("token");
if (token != null) {
// 用户名
String username = tokenManager.getUsernameFromToken(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (tokenManager.validateToken(token, userDetails)) {
//给使用该JWT令牌的用户进行授权
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(userDetails, token,
userDetails.getAuthorities());
return authenticationToken;
}
}
}
return null;
}
社交登录
1.流程图
a. 引导【client】跳转至【第三方应用-登录】进行【登录-授权】操作,成功后返回【code】
b. 【client】获取到【code】向【第三方应用-认证】换取【access_token】
c. 【client】获取到【access_token】向【第三方应用-资源】发起API请求,获取开放的【用户信息】
2.实现思路
a. 【登录功能】由【第三方平台】实现,登录成功后,需要将返回的信息保存入本地。与【本地用户】建立关联关系
b. 【认证功能】与jwt认证功能类似,只需要做微调
3.实现(本实例使用【新浪微博开放平台】)
1.平台地址:https://open.weibo.com/
2.应用信息
3.回调信息
4.操作步骤
5.access_token
【uid】用户标志id,需要用它与本地用户建立关联关系
【expires_in】access_token有效日期,生成jwt时有效期需要使用该属性
6.生成token
a. 在【第三方平台】的回调url中,获取到返回的code
b. 使用code,向【第三方平台】换取对应的access_token
c. access_token中的uid与本地用户User,建立对应关系
@RestController
@RequestMapping("/oauth")
public class OAuthController {
@Autowired
TokenManager tokenManager;
@Autowired
MyUserDetailsService myUserDetailsService;
// 应该存在redis中
public static Map<String, Social> socials = new HashMap<>();
private static final String KEY = "YOU APP ID";
private static final String SECRET = "YOU SECURITY";
@GetMapping("/token")
public String token(@RequestParam String code) {
// 获取access_token
String url = "https://api.weibo.com/oauth2/access_token";
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(url)
// Add query parameter
.queryParam("client_id", KEY)
.queryParam("client_secret", SECRET)
.queryParam("grant_type", "authorization_code")
.queryParam("redirect_uri", "http://192.168.0.14:9730/oauth/token")
.queryParam("code", code);
RestTemplate restTemplate = new RestTemplateBuilder().build();
Social social = restTemplate.postForObject(builder.toUriString(), null, Social.class);
socials.put(social.getUid(), social);
// 生成token
User user = new User("admin", "123", "社交登录id");
SecurityUser securityUser = new SecurityUser(user, social, null);
String token = tokenManager.generateToken(securityUser);
return token;
}
}
7.剩余部分
剩余部分与jwt大同小异,此处不再赘述,可自行查看源码
结束语:感谢大家的耐心阅读
代码下载