若依集成钉钉扫码登录

准备

钉钉文档地址: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();     } } 

测试

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

若依集成钉钉扫码登录

发表评论

相关文章