SpringSecurity多种登录方式,模拟手机验证码登录,实现Security中的Filter、Provider、Token,结果可以登录成功,但不能进行权限控制,获取Security上下文显示是匿名角色ROLE_ANONYMOUS,请问下面代码哪里理解有问题?应该如何修改?
SpringBoot3+SpringSecurity6+ajax
Filter
/**
* Filter负责拦截请求并调用Manager
* Manager负责管理多个Provider,并选择合适的Provider进行认证
* Provider负责认证,检查账号密码之类的
* Token是认证信息,包含账号密码,具体可以自己定义
*/
public class MyMultipleLoginFilter extends AbstractAuthenticationProcessingFilter {
private AuthenticationManager authenticationManager;
// 要拦截的登录请求
public MyMultipleLoginFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
public MyMultipleLoginFilter(AuthenticationManager authenticationManager){
super(new AntPathRequestMatcher("/login", "POST"));
this.authenticationManager = authenticationManager;
}
/**
* 封装信息,提交认证
* @param request
* @param response
* @return
* @throws AuthenticationException
* @throws IOException
* @throws ServletException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if("GET".equals(request.getMethod())){
throw new AuthenticationServiceException("MyMultipleLoginFilter method not supported: " + request.getMethod());
}else{
String phone = request.getParameter("phone");
String code = request.getParameter("code");
System.out.println("phone:" + phone + " code:" + code);
// 自定义token
MyMultipleAuthenticationToken token = new MyMultipleAuthenticationToken(phone, code);
return this.getAuthenticationManager().authenticate(token);
}
}
@Override
protected AuthenticationSuccessHandler getSuccessHandler() {
return super.getSuccessHandler();
}
}
Provider
/**
* 用于验证自定义的服务是否与预期一致,如用户输入的邮箱验证码和预期的验证码
*
* Filter负责拦截请求并调用Manager
* Manager负责管理多个Provider,并选择合适的Provider进行认证
* Provider负责认证,检查账号密码之类的
* Token是认证信息,包含账号密码,具体可以自己定义
*/
@Component
public class MyMultipleAuthenticationProvider implements AuthenticationProvider {
@Autowired
SecurityUserDetailsServiceImpl userDetailsService;
/**
* 验证信息
* @param authentication 封装的验证信息?token?
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 认证代码
// 转为自定义token,里面封装了认证时需要的数据
MyMultipleAuthenticationToken myAuthToken = (MyMultipleAuthenticationToken) authentication;
// 获取登录phone
String phone = (String) myAuthToken.getPrincipal();
// 获取用户输入凭证,如邮箱、手机验证码
String code = (String) myAuthToken.getCredentials();
if(phone.isEmpty() || code.isEmpty()){
return null;
}
// 查询数据库获取用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername(phone);
// 去数据库/redis查询验证码是否正确,然后判断抛出异常等情况
// 此处简写从数据库/redis读取验证码,仍以账号密码代替手机号和验证码,给明文密码加密以对比数据库中的密码
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
if(!passwordEncoder.matches(code, userDetails.getPassword())){
// throw new BadCredentialsException("验证码不正确");
throw new RuntimeException("验证码不正确");
}
// 返回认证后的token
MyMultipleAuthenticationToken authenticationToken = new MyMultipleAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
authenticationToken.setDetails(userDetails);
// 认证信息放入安全上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
return authenticationToken;
}
// 判断token支持类,验证传入的身份验证对象是否是指定的 xxToken
@Override
public boolean supports(Class<?> authentication) {
return MyMultipleAuthenticationToken.class.isAssignableFrom(authentication);
}
}
Token
/**
* 封装验证信息,根据自定义验证类型,如封装邮箱验证码信息
* Token的核心在于两个构造方法,一个是认证前使用,一个是认证后使用。
*
* Filter负责拦截请求并调用Manager
* Manager负责管理多个Provider,并选择合适的Provider进行认证
* Provider负责认证,检查账号密码之类的
* Token是认证信息,包含账号密码,具体可以自己定义
*/
public class MyMultipleAuthenticationToken extends AbstractAuthenticationToken {
// 如邮箱、手机号
private Object principal;
// 如邮箱验证码、手机验证码
private Object credentials;
/**
* 认证时过滤器通过这个方法创建Token,传入前端的参数
* @param credentials phone
* @param principal code
*/
public MyMultipleAuthenticationToken(Object principal, Object credentials){
super(null);
this.credentials = credentials;
this.principal = principal;
//关键:标记未认证
setAuthenticated(false);
}
/**
* 认证通过后Provider通过这个方法创建Token,传入自定义信息以及授权信息
* @param credentials phone
* @param principal code
* @param authorities List auth
*/
public MyMultipleAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.credentials = credentials;
this.principal = principal;
//关键:标记已认证
super.setAuthenticated(true);
}
public static MyMultipleAuthenticationToken authenticated(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
return new MyMultipleAuthenticationToken(principal, credentials, authorities);
}
@Override
public Object getCredentials() {
return credentials;
}
@Override
public Object getPrincipal() {
return principal;
}
}
Config
/**
* Security配置类
*/
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Autowired
SecurityUserDetailsServiceImpl userDetailsService;
// 自定义验证
@Autowired
MyMultipleAuthenticationProvider myMultipleAuthenticationProvider;
@Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Arrays.asList(myMultipleAuthenticationProvider));
}
@Bean
public MyMultipleLoginFilter myMultipleLoginFilter(){
MyMultipleLoginFilter myMultipleLoginFilter = new MyMultipleLoginFilter();
myMultipleLoginFilter.setAuthenticationManager(authenticationManager());
myMultipleLoginFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// response.getWriter().write("success!");
System.out.println("成功之后将auth放入上下文");
SecurityContextHolder.getContext().setAuthentication(authentication);
response.setContentType("text/json;charset=utf-8");
Result result = new Result();
result.setCode(1);
result.setStatus(222);
result.setMsg("登录成功!");
ObjectMapper mapper = new ObjectMapper();
ServletOutputStream outputStream = response.getOutputStream();
mapper.writeValue(outputStream, result);
// response.sendRedirect("/home");
}
});
return myMultipleLoginFilter;
}
/**
* 密码编码器
* @return
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth->{
// 设置url权限,注意所有权限的配置顺序
auth.requestMatchers("/home").permitAll();
auth.requestMatchers("/login").permitAll();
auth.requestMatchers("/phone_login").permitAll();
auth.requestMatchers("/login_phone").permitAll();
auth.requestMatchers("/test").permitAll();
// 验证码
auth.requestMatchers("/captcha/**").permitAll();
// 静态资源
auth.requestMatchers("/js/**").permitAll();
auth.requestMatchers("/home/l0").hasRole("USER");
auth.requestMatchers("/home/l1/**").hasRole("Dog");
auth.requestMatchers("/home/l2/**").hasRole("Cat");
auth.anyRequest().authenticated();
// auth.anyRequest().permitAll();
})
// 禁用session
.sessionManagement(session->{
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
})
.userDetailsService(userDetailsService)
.authenticationProvider(myMultipleAuthenticationProvider)
.addFilterBefore(myMultipleLoginFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf(conf -> {conf.disable();})// 关闭跨站请求伪造保护功能
.build();
}
}
登录响应
http://localhost:8080/login
{"code":1,"status":222,"msg":"登录成功!"}
打印上下文
@ResponseBody
@GetMapping("/test")
public Authentication test(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication;
}
{
"authorities": [
{
"authority": "ROLE_ANONYMOUS"
}
],
"details": {
"remoteAddress": "0:0:0:0:0:0:0:1",
"sessionId": null
},
"authenticated": true,
"principal": "anonymousUser",
"keyHash": -1412005665,
"credentials": "",
"name": "anonymousUser"
}
控制台数据登录时的认证,是有的
MyMultipleAuthenticationToken [Principal=$2a$10$5PHUgYh8A31715Vb0pjR3OWPiF8Y/Ns1P5BaSqGtD/YMC/EqyjCYa, Credentials=[PROTECTED], Authenticated=true, Details=org.springframework.security.core.userdetails.User [Username=pop, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_Cat]], Granted Authorities=[ROLE_Cat]]
在网上查找信息,有一篇文章提到是因为Security在认证后会把上下文清空,所以需要再添加一个过滤器手动将认证信息放入上下文中,但是不能在这个过滤器中获取登录信息
权限管理03-security登陆后鉴权_security登录验证与鉴权_高秉文的博客-CSDN博客
/**
* 解决天坑坑坑坑坑----------------
* 登录成功,一直401,显示匿名用户鉴权失败,是因为SecurityContextHolder自己把Context清楚了,我们需要重新设置一下
*
* @author gaorimao
* @date 2022/02/10
*/
@Component
public class MyOncePerRequestFilter extends OncePerRequestFilter {
@Autowired
SecurityUserDetailsServiceImpl userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, ServletException, IOException {
if(!"/login".equals(request.getRequestURI())){
// 获取登录phone,不能读取,只能先写死测试
// String phone = request.getParameter("phone");
// 获取用户输入凭证,如邮箱、手机验证码
// String code = request.getParameter("code");
String phone = "pop";
String code = "1";
System.out.println("手机验证:phone=" + phone + " code=" + code);
if(phone.isEmpty() || code.isEmpty()){
}
// 数据库校验用户名密码
UserDetails userDetails = userDetailsService.loadUserByUsername(phone);
/*
重新设置SecurityContextHolder
*/
MyMultipleAuthenticationToken myMultipleAuthenticationToken = new MyMultipleAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
myMultipleAuthenticationToken.setDetails(userDetails);
SecurityContextHolder.getContext().setAuthentication(myMultipleAuthenticationToken);
}
filterChain.doFilter(request, response);
}
}
登录认证机制可以自己实现,不用太依赖框架。
1)用户登录认证之后,将用户信息保存在本地浏览器中cookie(还可以设置过期时间),
2)后面每次发起http请求,都自动携带上该信息,就能达到认证用户,保持用户在线。
1)用户登录成功之后,在服务端就会生成一个键值对。key叫做sessionid,value就保存ssession(用户信息),客户端那边就需要把sessionid存储到cookie。
2)后续的http请求会携带上sessionid,服务器就根据sessionid来查找对应的信息。
1)用户登录成功之后,token 由服务器本身根据算法生成后下发给客户端,服务器端无需额外存储。
2)客户端请求服务器时,在请求头中追加携带该token
3)服务器端对token进行验签,从而决定本次访问是拒绝还是放行。
以上是我们常见的登录认证方式,但是在前后端分离的项目中,如今比较流行的还是JWT等。
详细参考这篇文章《Springboot 实战纪实》
可以看看 spring security 官方文档
用不用session好像都不行,看了文档,spring security6 中需要显式保存,是和这个有关系吗?没看出来具体要怎么做
https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html#requireexplicitsave
https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html#customizing-where-authentication-is-stored
@蔚然丶丶:
调试(debug)下。
从 过滤器链 开始处理请求开始(加断点)。
你是用了还是没用?我说的用 session 是指,普通模式下,前端会把 session id 传给后端。
spring security 6 我还没用过呢。
@蔚然丶丶:
看看下面的博文:
springBoot集成spring-security 自定义token实现
作者:lzq199528
于 2021-08-06 09:05:09 发布
————————————————
版权声明:本文为CSDN博主「lzq199528」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lzq199528/article/details/119443564
文末源码,自建了一个 TokenAndAuthentication 类。
@蔚然丶丶:
尝试禁用匿名身份验证
https://cloud.tencent.com/developer/ask/sof/420773/answer/676837
参考
– 蔚然丶丶 1年前https://stackoverflow.com/questions/64679097/spring-security-returning-anonymous-user-after-successful-authenticaiton
https://stackoverflow.com/questions/74341976/spring-security-an-authentication-object-was-not-found-in-the-securitycontext