在前端开发中,RESTful API 是一种非常常见的接口设计方式,而 OAuth2 认证授权机制则是一种较为安全、有效的接口认证方式。本文将详细介绍如何在 RESTful API 中使用 OAuth2 进行认证授权,实现安全可靠的接口访问。
OAuth2 简介
OAuth2 是一种基于授权的开放标准协议,用于授权第三方应用访问用户资源。它允许用户从第三方应用中选择授权,而不是将用户名和密码直接提供给第三方应用。
OAuth2 被广泛用于实现微信、QQ、微博等第三方登录,也是一些流行的云服务平台如 Google、Microsoft Azure 等所采用的认证授权机制。
OAuth2 标准协议主要包括:
- 授权码流程(Authorization Code Flow)
- 密码流程(Password Grant Flow)
- 客户端凭证流程(Client Credentials Flow)
本文以授权码流程为例进行介绍。
授权码流程
授权码流程是最为安全且最适用于 Web 应用程序的认证流程。其主要步骤如下:
应用程序将用户导向认证服务器 (Authorization Server),并申请授权。
用户同意授权,并在认证服务器上完成登录。
认证服务器将用户重定向回应用程序,同时附带一个授权码(Authorization Code)。
应用程序使用授权码向认证服务器请求身份令牌(Access Token)。
认证服务器验证授权码的有效性,如果有效则向应用程序发送身份令牌。
应用程序使用身份令牌请求用户资源。
用户资源服务器向应用程序返回用户资源。
其中,步骤 1 是通过重定向用户浏览器来实现的,因此需要指定一个重定向 URI。重定向 URI 可以是应用程序的一个特殊网页,该网页将收到包含授权码的服务器响应。
步骤 2 和 3 一般由认证服务器自动完成。
步骤 4 和 5 需要应用程序向认证服务器发送请求,因此需要应用程序的凭证(Client ID 和 Client Secret)。
步骤 6 和 7 中,应用程序需要使用身份令牌才能请求用户资源。身份令牌通常在 HTTP 头中发送,并使用 Bearer 类型指定。
OAuth2 实现
下面通过一个示例来演示如何在 RESTful API 中使用 OAuth2 进行认证授权。我们假定:
- 认证服务器采用 Spring Boot OAuth2
- 应用程序使用 Node.js + Express
- 用户资源服务器采用 MongoDB 作为数据库
认证服务器
首先我们需要搭建认证服务器,并定义 RESTful API 接口。
pom.xml
// javascriptcn.com 代码示例 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-server</artifactId> <version>2.6.1</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-mongodb</artifactId> </dependency> </dependencies>
application.yml
// javascriptcn.com 代码示例 spring: data: mongodb: uri: mongodb://localhost:27017/test server: port: 8080 logging: level: org: springframework: security: oauth2: server: DEBUG
AuthorizationServerConfiguration.java
// javascriptcn.com 代码示例 @Configuration @EnableAuthorizationServer public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { private final AuthenticationManager authenticationManager; private final PasswordEncoder passwordEncoder; private final MongoUserDetailsService mongoUserDetailsService; public AuthorizationServerConfiguration(AuthenticationManager authenticationManager, PasswordEncoder passwordEncoder, MongoUserDetailsService mongoUserDetailsService) { this.authenticationManager = authenticationManager; this.passwordEncoder = passwordEncoder; this.mongoUserDetailsService = mongoUserDetailsService; } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("client-id") .authorizedGrantTypes("authorization_code", "refresh_token") .authorities("ROLE_CLIENT") .scopes("read", "write") .accessTokenValiditySeconds(60 * 60) .refreshTokenValiditySeconds(60 * 60 * 24) .redirectUris("http://localhost:3000/callback"); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.checkTokenAccess("isAuthenticated()"); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager) .userDetailsService(mongoUserDetailsService) .accessTokenConverter(accessTokenConverter()) .tokenStore(tokenStore()); } private TokenStore tokenStore() { return new InMemoryTokenStore(); } private JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("OAUTH2_SECRET"); return converter; } @Bean public MongoClient mongoClient() { return new MongoClient("localhost", 27017); } @Bean public MongoTemplate mongoTemplate() { return new MongoTemplate(mongoClient(), "test"); } @Bean public MongoUserDetailsService mongoUserDetailsService() { return new MongoUserDetailsService(mongoTemplate(), passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
MongoUserDetailsService.java
// javascriptcn.com 代码示例 public class MongoUserDetailsService implements UserDetailsService { private final MongoTemplate mongoTemplate; private final PasswordEncoder passwordEncoder; public MongoUserDetailsService(MongoTemplate mongoTemplate, PasswordEncoder passwordEncoder) { this.mongoTemplate = mongoTemplate; this.passwordEncoder = passwordEncoder; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = mongoTemplate.findOne(Query.query(Criteria.where("username").is(username)), User.class); if (user == null) { throw new UsernameNotFoundException("User not found"); } else { List<SimpleGrantedAuthority> authorities = user.getRoles().stream() .map(role -> new SimpleGrantedAuthority(role)) .collect(Collectors.toList()); return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities); } } }
应用程序
然后我们需要搭建应用程序,并使用 oauth2orize 库实现 OAuth2 认证授权。
package.json
// javascriptcn.com 代码示例 { "dependencies": { "body-parser": "^1.18.3", "express": "^4.16.4", "mongodb": "^4.4.4", "oauth2orize": "^1.9.0", "passport": "^0.4.1", "passport-http-bearer": "^1.0.1" } }
server.js
// javascriptcn.com 代码示例 const express = require('express'); const bodyParser = require('body-parser'); const mongodb = require('mongodb'); const passport = require('passport'); const BearerStrategy = require('passport-http-bearer').Strategy; const oauth2orize = require('oauth2orize'); const db = mongodb.MongoClient.connect('mongodb://localhost:27017/test'); const server = oauth2orize.createServer(); passport.use(new BearerStrategy((token, done) => { db.then(client => { client.db().collection('access_tokens').findOne({ 'token': token }, (err, accessToken) => { if (err) { return done(err); } if (!accessToken) { return done(null, false); } client.db().collection('users').findOne({ '_id': accessToken.userId }, (err, user) => { if (err) { return done(err); } if (!user) { return done(null, false); } return done(null, user, { scope: 'all' }); }); }); }).catch(e => done(e)); })); server.exchange(oauth2orize.exchange.authorizationCode((clientId, redirectUri, code, done) => { db.then(client => { client.db().collection('authorization_codes').findOne({ 'code': code }, (err, authorizationCode) => { if (err) { return done(err); } if (!authorizationCode) { return done(null, false); } if (clientId !== authorizationCode.clientId) { return done(null, false); } if (redirectUri !== authorizationCode.redirectUri) { return done(null, false); } client.db().collection('access_tokens').insertOne({ 'token': oauth2orize.utils.uid(256), 'userId': authorizationCode.userId, 'clientId': authorizationCode.clientId, 'tokenType': 'Bearer', 'expiresIn': 3600 }, (err, result) => { if (err) { return done(err); } client.db().collection('authorization_codes').deleteOne({ 'code': code }); done(null, result.ops[0].token); }); }); }).catch(e => done(e)); })); server.serializeClient((client, done) => done(null, client._id)); server.deserializeClient((id, done) => { db.then(client => { client.db().collection('clients').findOne({ '_id': new mongodb.ObjectId(id) }, (err, client) => { if (err) { return done(err); } if (!client) { return done(null, false); } done(null, client); }); }).catch(e => done(e)); }); passport.use('bearer', new BearerStrategy((token, done) => { db.then(client => { client.db().collection('access_tokens').findOne({ 'token': token }, (err, accessToken) => { if (err) { return done(err); } if (!accessToken) { return done(null, false); } client.db().collection('users').findOne({ '_id': accessToken.userId }, (err, user) => { if (err) { return done(err); } if (!user) { return done(null, false); } return done(null, user, { scope: 'all' }); }); }); }).catch(e => done(e)); })); const authCodeGrant = oauth2orize.grant.code((client, redirectUri, user, ares, done) => { db.then(client => { client.db().collection('authorization_codes').insertOne({ 'code': oauth2orize.utils.uid(16), 'clientId': client._id, 'userId': user._id, 'redirectUri': redirectUri }, (err, result) => { if (err) { return done(err); } done(null, result.ops[0].code); }); }).catch(e => done(e)); }); const authorize = [ passport.authenticate('bearer', { session: false }), server.authorization((clientId, redirectUri, done) => { db.then(client => { client.db().collection('clients').findOne({ 'client_id': clientId }, (err, client) => { if (err) { return done(err); } if (!client) { return done(null, false) } if (!client.redirect_uri.includes(redirectUri)) { return done(null, false); } done(null, client, redirectUri); }); }).catch(e => done(e)) }, authCodeGrant) ]; const token = [ passport.authenticate('bearer', { session: false }), server.token(), server.errorHandler() ]; const app = express(); app.use(bodyParser.json()); app.get('/ping', (req, res) => res.send('pong')); app.get('/oauth/authorize', authorize); app.post('/oauth/token', token); app.listen(3000, () => console.log('Server listening on http://localhost:3000'));
用户资源服务器
最后我们需要搭建用户资源服务器,并定义 RESTful API 接口。
package.json
// javascriptcn.com 代码示例 { "dependencies": { "body-parser": "^1.18.3", "express": "^4.16.4", "mongodb": "^4.4.4", "passport": "^0.4.1", "passport-http-bearer": "^1.0.1" } }
server.js
// javascriptcn.com 代码示例 const express = require('express'); const bodyParser = require('body-parser'); const mongodb = require('mongodb'); const passport = require('passport'); const BearerStrategy = require('passport-http-bearer').Strategy; const db = mongodb.MongoClient.connect('mongodb://localhost:27017/test'); const app = express(); app.use(passport.initialize()); passport.use(new BearerStrategy((token, done) => { db.then(client => { client.db().collection('access_tokens').findOne({ 'token': token }, (err, accessToken) => { if (err) { return done(err); } if (!accessToken) { return done(null, false); } client.db().collection('users').findOne({ '_id': accessToken.userId }, (err, user) => { if (err) { return done(err); } if (!user) { return done(null, false); } return done(null, user, { scope: 'all' }); }); }); }).catch(e => done(e)); })); app.get('/protected', passport.authenticate('bearer', { session: false }), (req, res) => { res.send('This is a protected resource.'); }); app.get('/public', (req, res) => { res.send('This is a public resource.'); }); app.listen(4000, () => console.log('Server listening on http://localhost:4000'));
测试
现在我们完成了 OAuth2 认证授权的搭建,可以开始测试了。
- 启动认证服务器:
mvn spring-boot:run
- 启动应用程序服务器:
node server.js
- 启动用户资源服务器:
node server.js
- 访问应用程序:http://localhost:3000,输入用户名和密码进行登录。
- 点击 "Authorize" 按钮授权。
- 按照如下方式访问受保护资源:
- 成功:
curl http://localhost:4000/protected -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
- 失败:
curl http://localhost:4000/protected
可以看到,访问受保护资源时需要使用先前获取的访问令牌。如果访问令牌未过期且有效,则可以成功访问资源。否则,将收到未经授权的响应。
总结
本文详细介绍了如何在 RESTful API 中使用 OAuth2 进行认证授权。通过该认证授权机制,可以构建安全可靠的接口访问,保护用户数据免受恶意攻击。该方法不仅简单易懂,而且广泛适用于各种应用场景,是 RESTful API 开发中不可或缺的必要技术。
来源:JavaScript中文网 ,转载请注明来源 本文地址:https://www.javascriptcn.com/post/65409ec07d4982a6eba22ba6