準備
釘釘文件地址: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();
}
}
測試
資料庫給某個使用者賦值釘釘手機號,掃碼登入。