ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

若依集成钉钉扫码登录

2022-06-14 19:32:29  阅读:240  来源: 互联网

标签:扫码 code return String 登录 若依 state public


准备

钉钉文档地址:https://open.dingtalk.com/document/orgapp-server/scan-qr-code-to-log-on-to-third-party-websites

这个是历史版本的文档,最新版本的测试不稳定,经常出现系统繁忙。

按照钉钉文档做好前期准备,这里只说明若依框架代码的调整。

前端

页面

修改登陆页面src/views/login.vue,增加钉钉登录按钮


<el-form-item style="width:100%;">
    <el-button size="medium" type="primary" style="width:100%;" @click.native.prevent="ddLogin">
        <span>扫码登录</span>
    </el-button>
</el-form-item>


    ddLogin() {
      window.location.href = "https://oapi.dingtalk.com/connect/qrconnect?appid=your appid&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=your redirect_uri"
    }

新建页面src/views/sso.vue

<script>

export default {
    data() {
        return {
            code: '',
            state: ''
        }
    },
    created() {
        const { params, query } = this.$route
        this.code = query.code
        this.state = query.state
        // 钉钉
        this.$store.dispatch("SSO", { code: this.code, state: this.state }).then(() => {
            this.$router.push({ path: this.redirect || "/" }).catch(() => { });
        }).catch(() => {
            this.$message.error("系统异常,请稍后再试!");
        });
    },
    render: function (h) {
        return h() // avoid warning message
    }
}
</script>

修改路由文件src/router/index.js增加路由

// 公共路由
export const constantRoutes = [
  .....
  {
    path: '/sso',
    component: () => import('@/views/sso'),
    hidden: true
  },
   ..... 
]

修改src/permission.js,设置白名单

const whiteList = ['/login', '/auth-redirect', '/bind', '/register','/sso']

接口

新建src/api/sso.js

import request from '@/utils/request'

// 登录方法
export function sso(code,state) {
  const data = {
    code,
    state
  }
  return request({
    url: '/sso',
    headers: {
      isToken: false
    },
    method: 'post',
    data: data
  })
}

修改src/store/modules/user.js actions增加钉钉登录

//SSO
SSO({ commit }, info) { 
    const code = info.code
    const state = info.state
    return new Promise((resolve, reject) => {
        sso(code, state).then(res => {
            console.log('user.js sso')
            console.log(res)
            setToken(res.token)
            commit('SET_TOKEN', res.token)
            resolve()
        }).catch(error => {
            reject(error)
        })
    })
}

后端

因为我对项目目录结构进行了调整,这里就不说明放在那个包下,大家根据自己情况使用即可。

Controller

新建SSOController

@RestController
public class SSOController {

    @Resource
    private ISsoService ssoService;
    /**
     * 登录方法
     *
     * @param ssoBody 登录信息
     * @return 结果
     */
    @PostMapping("/sso")
    public AjaxResult sso(@RequestBody SSOBody ssoBody) throws ApiException {
        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        String token = ssoService.login(ssoBody.getCode(), ssoBody.getState(), ssoBody.getType());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }
}

新建SSOBody


/**
 * sso登录对象
 */
public class SSOBody {
    /**
     * 编码
     */
    private String code;

    /**
     * 状态码 可以用来判断是哪个系统
     */
    private String state;


    /**
     * 登录系统 后面可以换成枚举
     */
    private String type;

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getState() {
        return state;
    }
    public void setState(String state) {
        this.state = state;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }
}

Service

新建ISsoService

public interface ISsoService {

    /**
     * sso登录
     * @param code 登录码
     * @param state 状态码
     * @param type 登录系统 后面可以换成枚举
     * @return token
     */
    public String login(String code,String state,String type) throws ApiException;

    /**
     * 根据手机号获取用户
     * @param phonenumber 手机号
     * @return 用户
     */
    public UserDetails loadUserByPhonenumber(String phonenumber);
}

新建SsoServiceImpl

登录方法对应钉钉接口文档:https://open.dingtalk.com/document/orgapp-server/use-dingtalk-account-to-log-on-to-third-party-websites

@Service
public class SsoServiceImpl implements ISsoService {

    private static final Logger log = LoggerFactory.getLogger(SsoServiceImpl.class);

    @Autowired
    private ISysUserService userService;

    @Autowired
    private SysPermissionService permissionService;

    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private TokenService tokenService;

    /**
     * sso登录
     *
     * @param code  登录码
     * @param state 状态码
     * @param type  登录系统 后面可以换成枚举
     * @return token
     */
    @Override
    public String login(String code, String state, String type) throws ApiException {
        //if type = xxx ....如果多种验证方式

        // 获取access_token
        String access_token = AccessTokenUtil.getToken();

        // 通过临时授权码获取授权用户的个人信息
        DefaultDingTalkClient client2 = new DefaultDingTalkClient("https://oapi.dingtalk.com/sns/getuserinfo_bycode");
        OapiSnsGetuserinfoBycodeRequest reqBycodeRequest = new OapiSnsGetuserinfoBycodeRequest();
        // 通过扫描二维码,跳转指定的redirect_uri后,向url中追加的code临时授权码
        reqBycodeRequest.setTmpAuthCode(code);
        OapiSnsGetuserinfoBycodeResponse response = client.execute(reqBycodeRequest, "yourAppKey", "yourAppSecret");

        // 根据unionid获取userid
        String unionid = bycodeResponse.getUserInfo().getUnionid();
        DingTalkClient clientDingTalkClient = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/user/getbyunionid");
        OapiUserGetbyunionidRequest reqGetbyunionidRequest = new OapiUserGetbyunionidRequest();
        reqGetbyunionidRequest.setUnionid(unionid);
        OapiUserGetbyunionidResponse oapiUserGetbyunionidResponse = clientDingTalkClient.execute(reqGetbyunionidRequest, access_token);

        // 根据userId获取用户信息
        String userid = oapiUserGetbyunionidResponse.getResult().getUserid();
        DingTalkClient clientDingTalkClient2 = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/user/get");
        OapiV2UserGetRequest reqGetRequest = new OapiV2UserGetRequest();
        reqGetRequest.setUserid(userid);
        reqGetRequest.setLanguage("zh_CN");
        OapiV2UserGetResponse rspGetResponse = clientDingTalkClient2.execute(reqGetRequest, access_token);

        // 用户验证
        Authentication authentication = null;
        authentication = authenticationManager.authenticate(new DingDingAuthenticationToken(rspGetResponse.getResult().getMobile()));

        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(loginUser.getUsername(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.dd.login.success")));

        recordLoginInfo(loginUser.getUserId());
        // 生成token
        return tokenService.createToken(loginUser);
    }

    /**
     * 根据手机号获取用户
     *
     * @param phonenumber 手机号
     * @return 用户
     */
    @Override
    public UserDetails loadUserByPhonenumber(String phonenumber) {
        {
            SysUser user = userService.selectUserByPhonenumber(phonenumber);
            if (StringUtils.isNull(user)) {
                log.info("sso登录用户:{} 不存在.", phonenumber);
                throw new ServiceException("登录用户不存在");
            }

            return createLoginUser(user);
        }

    }

    public UserDetails createLoginUser(SysUser user) {
        return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
    }

    /**
     * 记录登录信息
     *
     * @param userId 用户ID
     */
    public void recordLoginInfo(Long userId) {
        SysUser sysUser = new SysUser();
        sysUser.setUserId(userId);
        sysUser.setLoginIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
        sysUser.setLoginDate(DateUtils.getNowDate());
        userService.updateUserProfile(sysUser);
    }
}

修改用户登录

修改ISysUserService增加通过手机号搜索用户方法

    /**
     * 通过手机号查询用户
     *
     * @param phonenumber 用户名
     * @return 用户对象信息
     */
    public SysUser selectUserByPhonenumber(String phonenumber);

实现


    /**
     * 通过手机号查询用户
     *
     * @param phonenumber 用户名
     * @return 用户对象信息
     */
    @Override
    public SysUser selectUserByPhonenumber(String phonenumber) {
        return userMapper.selectUserByPhonenumber(phonenumber);
    }

Mapper

    /**
     * 通过手机号查询用户
     *
     * @param phonenumber 用户名
     * @return 用户对象信息
     */
    public SysUser selectUserByPhonenumber(String phonenumber);
	<select id="selectUserByPhonenumber" parameterType="String"  resultMap="SysUserResult">
		<include refid="selectUserVo"/>
		where u.phonenumber = #{phonenumber}
	</select>

重点Spring Security

这里参考文章为:https://blog.csdn.net/dnf9906/article/details/113571941

SecurityConfig配置(framework/config/SecurityConfig.java)


/**
 * spring security配置
 * 
 * @author jelly
 */
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    private UserDetailsService userDetailsService;
    /** 钉钉 认证器*/
    @Autowired
    private DingDingAuthenticationProvider dingDingAuthenticationProvider;
    
    /**
     * 认证失败处理类
     */
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 退出处理类
     */
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * token认证过滤器
     */
    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;
    
    /**
     * 跨域过滤器
     */
    @Autowired
    private CorsFilter corsFilter;
    
    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 使用 permitAll() 方法所有人都能访问,包括带上 token 访问
                // 使用 anonymous() 所有人都能访问,但是带上 token 访问后会报错
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                .antMatchers("/login", "/register", "/captchaImage").anonymous()
                .antMatchers("/sso").anonymous()
                .antMatchers(
                        HttpMethod.GET,
                        "/",
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/profile/**"
                ).permitAll()
                .antMatchers("/swagger-ui.html").anonymous()
                .antMatchers("/swagger-resources/**").anonymous()
                .antMatchers("/webjars/**").anonymous()
                .antMatchers("/*/api-docs").anonymous()
                .antMatchers("/druid/**").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加CORS filter
        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
        // 新增 钉钉
        auth.authenticationProvider(dingDingAuthenticationProvider);
    }
}


主要修改了两个地方

.antMatchers("/sso").anonymous()
  // 新增 钉钉
auth.authenticationProvider(dingDingAuthenticationProvider);

新建Provider

新建DingDingAuthenticationProvider



/**
 * 钉钉登录
 */
@Component
public class DingDingAuthenticationProvider implements AuthenticationProvider {

    /** 钉钉登录验证服务 */
    @Autowired
    private ISsoService ssoService;

    /**
     * 进行认证
     *
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        long time = System.currentTimeMillis();
        System.out.println("钉钉登录验证");

        String phone = authentication.getName();
        // String rawCode = authentication.getCredentials().toString();

        // 1.根据手机号获取用户信息
        UserDetails userDetails = ssoService.loadUserByPhonenumber(phone);
        if (Objects.isNull(userDetails)) {
            throw new BadCredentialsException("钉钉当前用户未关联到系统用户");
        }
        // 3、返回经过认证的Authentication
        DingDingAuthenticationToken result = new DingDingAuthenticationToken(userDetails, Collections.emptyList());
        result.setDetails(authentication.getDetails());
        System.out.println("钉钉登录验证完成");
        return result;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        boolean res = DingDingAuthenticationToken.class.isAssignableFrom(authentication);
        System.out.println("钉钉进行登录验证 res:"+ res);
        return res;
    }
}

新建DingDingAuthenticationToken


public class DingDingAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // 手机号
    private final Object principal;
    /**
     * 此构造函数用来初始化未授信凭据.
     *
     * @param principal
     */
    public DingDingAuthenticationToken(Object principal) {
        super(null);
        System.out.println("DingDingAuthenticationToken1"+principal.toString());
        this.principal = principal;
        setAuthenticated(false);
    }
    /**
     * 此构造函数用来初始化授信凭据.
     *
     * @param principal
     */
    public DingDingAuthenticationToken(Object principal,Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        System.out.println("DingDingAuthenticationToken2"+principal.toString());
        this.principal = principal;
        super.setAuthenticated(true);
    }
    @Override
    public Object getCredentials() {
        return null;
    }

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

    @Override
    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");
        }
        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}

测试

数据库给某个用户赋值钉钉手机号,扫码登录。

钉钉扫码登录

标签:扫码,code,return,String,登录,若依,state,public
来源: https://www.cnblogs.com/jellydong/p/16376029.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有