认证
认证
只介绍账号密码登录获取Token的方式
官网demo
origin https://github.com/Azure-Samples/ms-identity-msal-java-samples.git (fetch)
origin https://github.com/Azure-Samples/ms-identity-msal-java-samples.git (push)
- 进入
client-side
, 选择Username-Password-Flow
- 这是一个标准的maven项目,进入
SRC
- 修改配置文件
下面配置信息,需要先去ms axure 上申请设置
CLIENT_ID=a416354d-83a7*********
USER_NAME=你的用户名(邮箱地址)
USER_PASSWORD=你的密码
# The below properties do not need to be changed for this sample
# In a real situation they would be used to affect authentication behavior, such as changing where token requests are
# sent by using a different authority URL
AUTHORITY=https://login.microsoftonline.com/租户ID/oauth2/v2.0/token
SCOPE=user.read
- 之后运行代码:
UsernamePasswordFlow#main
可以得到token
需要注意的是:账号密码获取token,需要开启一个配置: allow public client flows, 参考文档如下:
https://learn.microsoft.com/en-us/answers/questions/1539386/use-java-sdk-call-graph-api-with-aadsts7000218-err
https://blog.csdn.net/arthas777/article/details/132048620
当然,这个方式只适合读取office365的邮件,如果不是,那么读取不了。
以下所有请求都需要添加请求头, Authorization=上面获取的token
读取邮箱中所有邮件
https://graph.microsoft.com/v1.0/me/mailFolders('Inbox')/messages??$select=sender,subject,isRead
url 也可以替换如下:
- /me/mailFolders('Inbox')/messages?$sender,subject,isRead
- /userId/mailFolders('folderId')/messages?$sender,subject,isRead
参数解释:
- ?$select=sender,subject,isRead: 意思是查询的列表中只获取邮件中的这几个属性
返回响应解释
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('2f***')/mailFolders('Inbox')/messages(sender,subject,isRead)",
"value": [
{
"@odata.etag": "W/\"CKKBN5\"",
"id": "AAMzyAAA=",
"subject": "[Gig Story] 跨部门协作 创新AI未来 | Innovate the future of AI through cross-org collaboration ",
"isRead": false,
"sender": {
"emailAddress": {
"name": "Legs",
"address": "les@leo.com"
}
}
},
{
"@odata.etag": "W/F5\"",
"id": "AAMkADNoRMxUyxAAA=",
"subject": "[Sur]平台工程用户满意度调研",
"isRead": false,
"sender": {
"emailAddress": {
"name": "Developer Community",
"address": "developers@lo.com"
}
}
}
],
"@odata.nextLink": "https://graph.microsoft.com/v1.0/me/mailFolders('Inbox')/messages?%24select=sender%2csubject%2cisRead&%24top=10&%24skip=10"
}
- odata.context: 当前请求地址
- value 邮件列表
- id: 邮件id
- subject: 邮件主题
- isRead: 邮件是否读取标志
- sender: 发件人信息
- @odata.nextLink: 下一条消息请求地址
可以对比以下,我们请求的地址和ms返回的当前请求地址和吓一跳消息地址略有不同
- https://graph.microsoft.com/v1.0/me/mailFolders('Inbox')/messages?$select=sender,subject,isRead
- https://graph.microsoft.com/v1.0/$metadata#users('2fe3e6ce')/mailFolders('Inbox')/messages(sender,subject,isRead)
- https://graph.microsoft.com/v1.0/me/mailFolders('Inbox')/messages?%24select=sender%2Csubject%2CisRead&%24top=10&%24skip=10
区别就是 me 可以 替换成userId,也可以写成 user('用户id') 的方式
folderId 可以使用id 也可以 mailFolders('Inbox')
反正使用一种就可以了。
还有就是吓一跳连接 多了两个参数: &$top=10&$skip=10
其实就是分页参数 所以当你想要访问首页的时候,参数可以设置为:$top=10&$skip=0
第二页:$top=10&$skip=10
第三页:$top=10&$skip=20
获取所有文件夹
官方文档:https://learn.microsoft.com/zh-cn/graph/api/user-list-mailfolders?view=graph-rest-1.0&tabs=http
注意返回值中的下条数据地址:"@odata.nextLink": "https://graph.microsoft.com/v1.0/users/2feba56f-7d67-408f-a254-309ffa23e6ce/mailFolders?%24skip=10"
这里是 skip, 和上面的分页参数是不一样的。
指定文件夹中的所有消息
这个貌似和上面的一样了
- https://learn.microsoft.com/zh-cn/graph/api/user-list-messages?view=graph-rest-1.0&tabs=http
- https://graph.microsoft.com/v1.0/users/用户id/mailFolders/文件夹id/messages
查看邮件详情
- https://graph.microsoft.com/v1.0/users/用户id/mailFolders/文件夹ID/messages/消息ID
- https://learn.microsoft.com/zh-cn/graph/api/mailfolder-list-messages?view=graph-rest-1.0&tabs=java#code-try-1
修改邮件
PATCH : https://graph.microsoft.com/v1.0/users/用户ID/mailFolders/文件夹iD/messages/消息ID
request body:
{
"isRead": true
}
代表将邮件中的isRead 属性改成true,代表当前邮件已读。
注意请求方式使用patch
/**
* office 365 client, use for call ms graph api
*/
@FeignClient(name = "office365GraphClient", url = "https://graph.microsoft.com/v1.0"
, configuration = Office365ClientConfiguration.class
)
public interface Office365Client {
@GetMapping("/me")
MsUserInfoDTO me();
@GetMapping(path = "/users/{id}/mailFolders?skip={size}")
OfficeFolderDTO folders(@PathVariable("id") String id, @PathVariable("size") int size);
@GetMapping(path = "/users/{userId}/mailFolders/{folderId}/messages/?%24top={top}&%24skip={skip}")
EmailDTO emails(@PathVariable("userId") String id, @PathVariable("folderId") String folderId,
@PathVariable("top") int top,
@PathVariable("skip") int skip);
// 标记已读
@PatchMapping(path = "/users/{userId}/mailFolders/{folderId}/messages/{messageId}")
EmailDTO markAsRead(@PathVariable("userId") String userId,
@PathVariable("folderId") String folderId,
@PathVariable("messageId") String messageId);
}
Office365ClientConfiguration.java
import com.microsoft.aad.msal4j.*;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import java.io.IOException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class Office365ClientConfiguration implements RequestInterceptor {
private static final Logger log = LoggerFactory.getLogger(Office365ClientConfiguration.class);
final StringRedisTemplate redisTemplate = ApplicationContextUtil.getBean(StringRedisTemplate.class);
final String cacheKey = RedisConstant.OFFICE_365_TOKEN;
@Override
public void apply(RequestTemplate requestTemplate) {
String url = requestTemplate.feignTarget().url();
if (url.contains("graph.microsoft.com")) {
try {
requestTemplate.header("Authorization", getToken());
} catch (Exception e) {
log.error("获取office365 token失败", e);
throw new ManagedServiceException("获取office365 token失败"+e.getMessage());
}
}
}
public String getToken() throws Exception {
ValueOperations<String,String> ops = redisTemplate.opsForValue();
String token = ops.get(cacheKey);
if (StringUtil.isBlank(token)){
IAuthenticationResult authResult = auth();
token = authResult.accessToken();
Instant expireTime = authResult.expiresOnDate().toInstant();
long duration = ChronoUnit.SECONDS.between(Instant.now(), expireTime);
ops.set(cacheKey, token, duration, TimeUnit.SECONDS);
}
return token;
}
public String getTokenFromCache(){
RedisTemplate redisTemplate = ApplicationContextUtil.getBean(RedisTemplate.class);
ValueOperations ops = redisTemplate.opsForValue();
return null;
}
private static String authority;
private static Set<String> scope;
private static String clientId;
private static String username;
private static String password;
public static IAuthenticationResult auth() throws Exception {
setUpSampleData();
PublicClientApplication pca = PublicClientApplication.builder(clientId)
.authority(authority)
.build();
//Get list of accounts from the application's token cache, and search them for the configured username
//getAccounts() will be empty on this first call, as accounts are added to the cache when acquiring a token
Set<IAccount> accountsInCache = pca.getAccounts().join();
IAccount account = getAccountByUsername(accountsInCache, username);
//Attempt to acquire token when user's account is not in the application's token cache
IAuthenticationResult result = acquireTokenUsernamePassword(pca, scope, account, username, password);
System.out.println("Account username: " + result.account().username());
System.out.println("Access token: " + result.accessToken());
System.out.println("Id token: " + result.idToken());
System.out.println();
// accountsInCache = pca.getAccounts().join();
// account = getAccountByUsername(accountsInCache, username);
//
// //Attempt to acquire token again, now that the user's account and a token are in the application's token cache
// result = acquireTokenUsernamePassword(pca, scope, account, username, password);
// System.out.println("Account username: " + result.account().username());
// System.out.println("Access token: " + result.accessToken());
// System.out.println("Id token: " + result.idToken());
return result;
}
private static IAuthenticationResult acquireTokenUsernamePassword(PublicClientApplication pca,
Set<String> scope,
IAccount account,
String username,
String password) throws Exception {
IAuthenticationResult result;
try {
SilentParameters silentParameters =
SilentParameters
.builder(scope)
.account(account)
.build();
// Try to acquire token silently. This will fail on the first acquireTokenUsernamePassword() call
// because the token cache does not have any data for the user you are trying to acquire a token for
result = pca.acquireTokenSilently(silentParameters).join();
System.out.println("==acquireTokenSilently call succeeded");
} catch (Exception ex) {
if (ex.getCause() instanceof MsalException) {
System.out.println("==acquireTokenSilently call failed: " + ex.getCause());
UserNamePasswordParameters parameters =
UserNamePasswordParameters
.builder(scope, username, password.toCharArray())
.build();
// Try to acquire a token via username/password. If successful, you should see
// the token and account information printed out to console
result = pca.acquireToken(parameters).join();
System.out.println("==username/password flow succeeded");
} else {
// Handle other exceptions accordingly
throw ex;
}
}
return result;
}
/**
* Helper function to return an account from a given set of accounts based on the given username,
* or return null if no accounts in the set match
*/
private static IAccount getAccountByUsername(Set<IAccount> accounts, String username) {
if (accounts.isEmpty()) {
System.out.println("==No accounts in cache");
} else {
System.out.println("==Accounts in cache: " + accounts.size());
for (IAccount account : accounts) {
if (account.username().equals(username)) {
return account;
}
}
}
return null;
}
/**
* Helper function unique to this sample setting. In a real application these wouldn't be so hardcoded, for example
* values such as username/password would come from the user, and different users may require different scopes
*/
private static void setUpSampleData() throws IOException {
// Load properties file and set properties used throughout the sample
authority = ApplicationContextUtil.getProperty("office365.authority");
scope = Collections.singleton(ApplicationContextUtil.getProperty("office365.scope"));
clientId = ApplicationContextUtil.getProperty("office365.clientId");
username = ApplicationContextUtil.getProperty("office365.username");
password = ApplicationContextUtil.getProperty("office365.password");
}
}
使用:
MsUserInfoDTO me = office365Client.me();
OfficeFolderDTO folderDTO = office365Client.folders(me.getId(), 0);
OfficeFolderDTO.Folder inboxFolder = folderDTO.getInboxFolder();
if (Objects.isNull(inboxFolder)) {
int size = 0;
while (Objects.isNull(inboxFolder)) {
size += 10;
folderDTO = office365Client.folders(me.getId(), size);
// 只读收件箱 Inbox 或者 收件箱
inboxFolder = folderDTO.getInboxFolder();
}
}
// 收件箱id
String inboxFolderId = inboxFolder.getId();
int top = 10;
int skip = 0;
EmailDTO emailDTOS = office365Client.emails(me.getId(), inboxFolderId, top, skip);
// 所有未读消息
List<EmailDTO.Value> collect = emailDTOS.getValue().stream()
.filter(Objects::nonNull)
.filter(EmailDTO.Value::notRead)
.collect(Collectors.toList());
// 标记为已读
office365Client.markAsRead(me.getId(), inboxFolderId, mail.getId());
// 中间省略 解析 下一条连接时,获取参数的过程,或者自己直接拼死每页10条,
// 当无数据时,默认不会返回这个key, 因此只要is null 就可以结束循环
while ((emailDTOS = office365Client.emails(me.getId(), inboxFolderId, top, skip)) != null && flag);
// 我使用的是do while 循环.. 代码不全,只有大概的代码框架,