首页 新闻 会员 周边 捐助

SpringSecurity自定义多种登录方式,登录成功之后没有认证信息,是匿名角色

0
[待解决问题]

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);
    }


}
< >
分享
所有回答(2)
0

登录认证机制可以自己实现,不用太依赖框架。

常见的登录实现方式

  • cookie

1)用户登录认证之后,将用户信息保存在本地浏览器中cookie(还可以设置过期时间),

2)后面每次发起http请求,都自动携带上该信息,就能达到认证用户,保持用户在线。

  • cookie+session

1)用户登录成功之后,在服务端就会生成一个键值对。key叫做sessionid,value就保存ssession(用户信息),客户端那边就需要把sessionid存储到cookie。

2)后续的http请求会携带上sessionid,服务器就根据sessionid来查找对应的信息。

  • token

1)用户登录成功之后,token 由服务器本身根据算法生成后下发给客户端,服务器端无需额外存储。

2)客户端请求服务器时,在请求头中追加携带该token

3)服务器端对token进行验签,从而决定本次访问是拒绝还是放行。

以上是我们常见的登录认证方式,但是在前后端分离的项目中,如今比较流行的还是JWT等。

详细参考这篇文章《Springboot 实战纪实》

楠木大叔 | 园豆:2083 (老鸟四级) | 2023-09-13 22:14
0

ajax 传的请求头 不对?
基于cookie+session传吗?
有特别的请求头?

可以看看 spring security 官方文档

快乐的欧阳天美1114 | 园豆:4010 (老鸟四级) | 2023-09-14 13:18

@蔚然丶丶:
调试(debug)下。
从 过滤器链 开始处理请求开始(加断点)。

用不用session好像都不行

你是用了还是没用?我说的用 session 是指,普通模式下,前端会把 session id 传给后端。

spring security 6 我还没用过呢。

支持(0) 反对(0) 快乐的欧阳天美1114 | 园豆:4010 (老鸟四级) | 2023-09-14 16:41

@蔚然丶丶:
看看下面的博文:
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 类。

支持(0) 反对(0) 快乐的欧阳天美1114 | 园豆:4010 (老鸟四级) | 2023-09-15 06:05

@蔚然丶丶:
尝试禁用匿名身份验证
https://cloud.tencent.com/developer/ask/sof/420773/answer/676837

支持(0) 反对(0) 快乐的欧阳天美1114 | 园豆:4010 (老鸟四级) | 2023-09-15 06:07
清除回答草稿
   您需要登录以后才能回答,未注册用户请先注册