准备

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

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

前端

页面

修改登陆页面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();
    }
}

测试

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

若依集成钉钉扫码登录-LMLPHP

06-14 19:27