准备
这个是历史版本的文档,最新版本的测试不稳定,经常出现系统繁忙。
按照钉钉文档做好前期准备,这里只说明若依框架代码的调整。
前端
页面
修改登陆页面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();
}
}
测试
数据库给某个用户赋值钉钉手机号,扫码登录。