Spring Cloud OAuth2 扩展登录方式:帐户密码登录、 手机验证码登录、 二维码扫码登录

本文扩展了spring security 的登录方式,增长手机验证码登录、二维码登录。 主要实现方式为使用自定义filter、 AuthenticationProvider、 AbstractAuthenticationToken 根据不一样登录方式分别处理

srping security 登录流程

关于二维码登录

二维码扫码登录前提是已在微信端登录,流程以下:github

  • 用户点击二维码登录,调用后台接口生成二维码(带参数key), 返回二维码连接、key到页面
  • 页面显示二维码,提示扫码,并经过此key创建websocket
  • 用户扫码,获取参数key,点击登录调用后台并传递key
  • 后台根据微信端用户登录状态拿到userdetail, 并在缓存(redis)中维护 key: userDetail 关联关系
  • 后台根据websocket: key通知对于前台页面登录
  • 页面用此key登录
    最后一步用户经过key登录就是本文的二维码扫码登录部分,实际过程当中注意二维码超时,redis超时等处理

自定义LoginFilter

@Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        // 登录类型:user:用户密码登录;phone:手机验证码登录;qr:二维码扫码登录
        String type = obtainParameter(request, "type");
        String mobile = obtainParameter(request, "mobile");
        MyAuthenticationToken authRequest;
        String principal;
        String credentials;

        // 手机验证码登录
        if("phone".equals(type)){
            principal = obtainParameter(request, "phone");
            credentials = obtainParameter(request, "verifyCode");
        }
        // 二维码扫码登录
        else if("qr".equals(type)){
            principal = obtainParameter(request, "qrCode");
            credentials = null;
        }
        // 帐号密码登录
        else {
            principal = obtainParameter(request, "username");
            credentials = obtainParameter(request, "password");
            if(type == null)
                type = "user";
        }
        if (principal == null) {
            principal = "";
        }
        if (credentials == null) {
            credentials = "";
        }
        principal = principal.trim();
        authRequest = new MyAuthenticationToken(
                principal, credentials, type, mobile);
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    private void setDetails(HttpServletRequest request,
                            AbstractAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

    private String obtainParameter(HttpServletRequest request, String parameter) {
        return request.getParameter(parameter);
    }

自定义 AbstractAuthenticationToken

继承 AbstractAuthenticationToken,添加属性 type,用于后续判断。

public class MyAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = 110L;
    private final Object principal;
    private Object credentials;
    private String type;
    private String mobile;

    /**
     * This constructor can be safely used by any code that wishes to create a
     * <code>UsernamePasswordAuthenticationToken</code>, as the {@link
     * #isAuthenticated()} will return <code>false</code>.
     *
     */
    public MyAuthenticationToken(Object principal, Object credentials,String type, String mobile) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        this.type = type;
        this.mobile = mobile;
        this.setAuthenticated(false);
    }

    /**
     * This constructor should only be used by <code>AuthenticationManager</code> or <code>AuthenticationProvider</code>
     * implementations that are satisfied with producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
     * token token.
     *
     * @param principal
     * @param credentials
     * @param authorities
     */
    public MyAuthenticationToken(Object principal, Object credentials,String type, String mobile, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        this.type = type;
        this.mobile = mobile;
        super.setAuthenticated(true);
    }


    @Override
    public Object getCredentials() {
        return this.credentials;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    public String getType() {
        return this.type;
    }

    public String getMobile() {
        return this.mobile;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if(isAuthenticated) {
            throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        } else {
            super.setAuthenticated(false);
        }
    }

    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}

自定义 AuthenticationProvider

实现 AuthenticationProvider

代码与 AbstractUserDetailsAuthenticationProvider 基本一致,只需修改 authenticate 方法 及 createSuccessAuthentication 方法中的 UsernamePasswordAuthenticationToken 为咱们的 token, 改成:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 此处修改断言自定义的 MyAuthenticationToken
        Assert.isInstanceOf(MyAuthenticationToken.class, authentication, this.messages.getMessage("MyAbstractUserDetailsAuthenticationProvider.onlySupports", "Only MyAuthenticationToken is supported"));
        // ...
    }

    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
        MyAuthenticationToken result = new MyAuthenticationToken(principal, authentication.getCredentials(),((MyAuthenticationToken) authentication).getType(),((MyAuthenticationToken) authentication).getMobile(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        return result;
    }

继承provider

继承咱们自定义的AuthenticationProvider,编写验证方法additionalAuthenticationChecks及 retrieveUser缓存

/**
     * 自定义验证
     * @param userDetails
     * @param authentication
     * @throws AuthenticationException
     */
    protected void additionalAuthenticationChecks(UserDetails userDetails, MyAuthenticationToken authentication) throws AuthenticationException {
        Object salt = null;
        if(this.saltSource != null) {
            salt = this.saltSource.getSalt(userDetails);
        }

        if(authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("MyAbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();

            // 验证开始
            if("phone".equals(authentication.getType())){
                // 手机验证码验证,调用公共服务查询后台验证码缓存: key 为authentication.getPrincipal()的value, 并判断其与验证码是否匹配,
                此处写死为 1000
                if(!"1000".equals(presentedPassword)){
                    this.logger.debug("Authentication failed: verifyCode does not match stored value");
                    throw new BadCredentialsException(this.messages.getMessage("MyAbstractUserDetailsAuthenticationProvider.badCredentials", "Bad verifyCode"));
                }
            }else if(MyLoginAuthenticationFilter.SPRING_SECURITY_RESTFUL_TYPE_QR.equals(authentication.getType())){
                // 二维码只须要根据 qrCode 查询到用户便可,因此此处无需验证
            }
            else {
                // 用户名密码验证
                if(!this.passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) {
                    this.logger.debug("Authentication failed: password does not match stored value");
                    throw new BadCredentialsException(this.messages.getMessage("MyAbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
                }
            }
        }
    }

    protected final UserDetails retrieveUser(String username, MyAuthenticationToken authentication) throws AuthenticationException {
        UserDetails loadedUser;
        try {
            // 调用loadUserByUsername时加入type前缀
            loadedUser = this.getUserDetailsService().loadUserByUsername(authentication.getType() + ":" + username);
        } catch (UsernameNotFoundException var6) {
            if(authentication.getCredentials() != null) {
                String presentedPassword = authentication.getCredentials().toString();
                this.passwordEncoder.isPasswordValid(this.userNotFoundEncodedPassword, presentedPassword, (Object)null);
            }

            throw var6;
        } catch (Exception var7) {
            throw new InternalAuthenticationServiceException(var7.getMessage(), var7);
        }

        if(loadedUser == null) {
            throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
        } else {
            return loadedUser;
        }
    }

自定义 UserDetailsService

查询用户时根据类型采用不一样方式查询: 帐号密码根据用户名查询用户; 验证码根据 phone查询用户, 二维码可调用公共服务微信

@Override
    public UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException {

        BaseUser baseUser;
        String[] parameter = var1.split(":");
        // 手机验证码调用FeignClient根据电话号码查询用户
        if("phone".equals(parameter[0])){
            ResponseData<BaseUser> baseUserResponseData = baseUserService.getUserByPhone(parameter[1]);
            if(baseUserResponseData.getData() == null || !ResponseCode.SUCCESS.getCode().equals(baseUserResponseData.getCode())){
                logger.error("找不到该用户,手机号码:" + parameter[1]);
                throw new UsernameNotFoundException("找不到该用户,手机号码:" + parameter[1]);
            }
            baseUser = baseUserResponseData.getData();
        } else if("qr".equals(parameter[0])){
            // 扫码登录根据key从redis查询用户
            baseUser = null;
        } else {
            // 帐号密码登录调用FeignClient根据用户名查询用户
            ResponseData<BaseUser> baseUserResponseData = baseUserService.getUserByUserName(parameter[1]);
            if(baseUserResponseData.getData() == null || !ResponseCode.SUCCESS.getCode().equals(baseUserResponseData.getCode())){
                logger.error("找不到该用户,用户名:" + parameter[1]);
                throw new UsernameNotFoundException("找不到该用户,用户名:" + parameter[1]);
            }
            baseUser = baseUserResponseData.getData();
        }

        // 调用FeignClient查询角色
        ResponseData<List<BaseRole>> baseRoleListResponseData = baseRoleService.getRoleByUserId(baseUser.getId());
        List<BaseRole> roles;
        if(baseRoleListResponseData.getData() == null ||  !ResponseCode.SUCCESS.getCode().equals(baseRoleListResponseData.getCode())){
            logger.error("查询角色失败!");
            roles = new ArrayList<>();
        }else {
            roles = baseRoleListResponseData.getData();
        }

        //调用FeignClient查询菜单
        ResponseData<List<BaseModuleResources>> baseModuleResourceListResponseData = baseModuleResourceService.getMenusByUserId(baseUser.getId());

        // 获取用户权限列表
        List<GrantedAuthority> authorities = convertToAuthorities(baseUser, roles);

        // 存储菜单到redis
        if( ResponseCode.SUCCESS.getCode().equals(baseModuleResourceListResponseData.getCode()) && baseModuleResourceListResponseData.getData() != null){
            resourcesTemplate.delete(baseUser.getId() + "-menu");
            baseModuleResourceListResponseData.getData().forEach(e -> {
                resourcesTemplate.opsForList().leftPush(baseUser.getId() + "-menu", e);
            });
        }

        // 返回带有用户权限信息的User
        org.springframework.security.core.userdetails.User user =  new org.springframework.security.core.userdetails.User(baseUser.getUserName(),
                baseUser.getPassword(), isActive(baseUser.getActive()), true, true, true, authorities);
        return new BaseUserDetail(baseUser, user);
    }

 

配置WebSecurityConfigurerAdapter

将咱们自定义的类配置到spring security 登录流程

@Configuration
@Order(ManagementServerProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 自动注入UserDetailsService
    @Autowired
    private BaseUserDetailService baseUserDetailService;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http    // 自定义过滤器
                .addFilterAt(getMyLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                // 配置登录页/login并容许访问
                .formLogin().loginPage("/login").permitAll()
                // 登出页
                .and().logout().logoutUrl("/logout").logoutSuccessUrl("/backReferer")
                // 其他全部请求所有须要鉴权认证
                .and().authorizeRequests().anyRequest().authenticated()
                // 因为使用的是JWT,咱们这里不须要csrf
                .and().csrf().disable();
    }

    /**
     * 用户验证
     * @param auth
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(myAuthenticationProvider());
    }

    /**
     * 自定义密码验证
     * @return
     */
    @Bean
    public MyAuthenticationProvider myAuthenticationProvider(){
        MyAuthenticationProvider provider = new MyAuthenticationProvider();
        // 设置userDetailsService
        provider.setUserDetailsService(baseUserDetailService);
        // 禁止隐藏用户未找到异常
        provider.setHideUserNotFoundExceptions(false);
        // 使用BCrypt进行密码的hash
        provider.setPasswordEncoder(new BCryptPasswordEncoder(6));
        return provider;
    }

    /**
     * 自定义登录过滤器
     * @return
     */
    @Bean
    public MyLoginAuthenticationFilter getMyLoginAuthenticationFilter() {
        MyLoginAuthenticationFilter filter = new MyLoginAuthenticationFilter();
        try {
            filter.setAuthenticationManager(this.authenticationManagerBean());
        } catch (Exception e) {
            e.printStackTrace();
        }
        filter.setAuthenticationSuccessHandler(new MyLoginAuthSuccessHandler());
        filter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/login?error"));
        return filter;
    }
}

 

 

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Spring Cloud OAuth2是一个基于OAuth2实现的身份认证和授权框架,可以用于实现单点登录(SSO)功能。 单点登录是指在一个系统中登录后,可以在其他关联系统中自动登录,无需再次输入账号和密码。使用Spring Cloud OAuth2可以实现这样的功能。 首先,需要在认证服务器上使用Spring Security和Spring Cloud OAuth2的组件搭建一个OAuth2认证服务。该服务会负责用户的认证和授权工作。 在各个子系统中,需要引入Spring Cloud OAuth2的客户端,然后配置认证服务器的地址和客户端的凭证信息(clientId和clientSecret)。 当用户访问某个子系统时,子系统会重定向到认证服务器进行认证。用户在认证服务器上输入账号和密码进行认证,认证成功后,认证服务器会返回一个授权码给子系统。 子系统将授权码发送给认证服务器,认证服务器通过校验授权码的有效性,并且根据授权码发放一个访问令牌。子系统使用访问令牌进行后续的接口访问。 当用户在其他关联系统中访问时,这些系统会共享认证服务器上的会话信息,无需再次进行登录认证,直接使用之前的访问令牌进行接口访问。 通过以上步骤,就实现了Spring Cloud OAuth2的单点登录功能。用户只需要在一个系统登录一次,就可以在其他系统中自动登录,提高了用户体验。同时,认证服务器集中管理用户的认证和授权信息,提供了一种便捷的集中式身份管理方式

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

闫焕俊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值