准备
钉钉文档地址: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(); } }
测试
数据库给某个用户赋值钉钉手机号,扫码登录。