OAuth2
OAuth2是当前授权的行业标准,其重点在于为Web应用程序、桌面应用程序、移动设备以及室内设备的授权流程提供简单的客户端开发方式。它为第三方应用提供对HTTP服务的有限访问,既可以是资源拥有者通过授权允许第三方应用获取HTTP服务,也可以是第三方以自己的名义获取访问权限。
角色
OAuth2中主要分为了4种角色:
- Resource Owner(资源所有者),是能够对受保护的资源授予访问权限的实体,可以是一个用户,这时会称为终端用户(end-user)。
- Resource Server(资源服务器),持有受保护的资源,允许持有访问令牌(Access Token)的请求访问受保护资源。
- Client(客户端),持有资源所有者的授权,代表资源所有者对受保护资源进行访问。
- Authorization Server(授权服务器),对资源所有者的授权进行认证,成功后向客户端发送访问令牌。
很多时候,资源服务器和授权服务器是合二为一的,在授权交互的时候作为授权服务器,在请求资源交互时作为资源服务器
Resource Server的配置
Resource Server(可以是授权服务器,也可以是其他的资源服务)提供了受OAuth2保护的资源,这些资源为API接口、Html页面、Js文件等.Spring OAuth2提供了实现此保护功能的Spring Security认证过滤器。在加了@Configuration注解的配置类上加@EnableResourceServer注解,开启Resource Server的功能
JWT
JSON Web Token(JWT)是一种开放的标准(RFC 7519),JWT定义了一种紧凑且自包含的标准,该标准旨在将各个主体的信息包装为JSON对象。主体信息是通过数字签名进行加密和验证的。常使用HMAC算法或RSA(公钥/私钥的非对称性加密)算法对JWT进行签名,安全性很高。
- 紧凑性(compact):由于是加密后的字符串,JWT数据体积非常小,可通过POST请求参数或HTTP请求头发送。另外,数据体积小意味着传输速度很快。
- 自包含(self-contained):JWT包含了主体的所有信息,所以避免了每个请求都需要向Uaa服务验证身份,降低了服务器的负载。
JWT由3个部分组成,分别以“.”分隔,组成部分如下。
- Header(头):Header通常由两部分组成:令牌的类型(即JWT)和使用的算法类型,如HMAC、SHA256和RSA
- Payload(有效载荷):了用户的一些信息和Claim(声明、权利)。有3种类型的Claim:保留、公开和私人
- Signature(签名):需要将Base64编码后的Header、Payload和密钥进行签名
uaa配置
1. 依赖:
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.2'
implementation 'org.springframework.cloud:spring-cloud-starter-oauth2:2.2.2.RELEASE'
implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery:2.2.2.RELEASE'
runtimeOnly 'mysql:mysql-connector-java'
2. 配置文件:
bootstrap.yml:
spring:
application:
name: consul-auth
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
application.yml:
server:
port: 8731
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ffzs?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
username: root
password: 123zxc
main:
allow-bean-definition-overriding: true
3. 安装mysql数据库
需要通过数据库进行对人员登录人员数据进行存储这里使用了Mysql数据库
使用docker安装:
mysql:
image: mysql
networks:
- spring
restart: always
ports:
- 33060:33060
- 3306:3306
volumes:
- ./mysql/db:/var/lib/mysql
- ./mysql/conf.d:/etc/mysql/conf.d
environment:
- MYSQL_ROOT_PASSWORD=123zxc
command: --default-authentication-plugin=mysql_native_password
创建一个user的表用来存储人员数据:
CREATE TABLE `myUser` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`password` varchar(255) DEFAULT NULL,
`username` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
BEGIN;
INSERT INTO `myUser` VALUES ('1', '$2a$10$uuFQKbr2q/8aqYlPEBlRw.Z9UrtEPrydIh7IUXaEGVWBowY8mZrUq', 'ffzs'),('2', '$2a$10$QgQ9OtiCMnGzYGPabDzOkeBda0Sb8wqzwnTSErJWPx4GfeNOUvh7q', 'sleepycat');
COMMIT;
添加两个用户
4. AuthorizationServer配置
/**
* @author ffzs
* @describe
* @date 2020/6/7
*/
@Configuration
@EnableAuthorizationServer
public class OAuthConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsServiceImpl userServiceDetail;
/**
* 配置客户端信息
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("consul_server") // 用户端id 需要在Authorization Server中是唯一的。
.secret("123456") // 连接密码
.scopes("server") // 配置的客户端域为 service
// .autoApprove(true) // client_secret
// .authorities("ROLE_ADMIN", "ROLE_USER") // 权限信息
.authorizedGrantTypes("implicit", "refresh_token", "password", "authorization_code", "client_credentials") // 类验证类型
.accessTokenValiditySeconds(60*60); //失效时间
}
/**
* 配置授权Token的节点和Token服务
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager) //只有配置了该选项,密码认证才会开启。在大多数情况下都是密码验证,所以一般都会配置这个选项
/**
* 需要设置Token的管理策略,目前支持以下3种:
* InMemoryTokenStore:Token存储在内存中。
* JdbcTokenStore:Token存储在数据库中。需要引入spring-jdbc的依赖包,并配置数据源,以及初始化Spring OAuth2的数据库脚本
* JwtTokenStore:采用JWT形式,这种形式没有做任何的存储,因为JWT本身包含了用户验证的所有信息,不需要存储。采用这种形式,需要引入spring-jwt的依赖
*/
.tokenStore(jwtTokenStore())
.tokenEnhancer(jwtTokenEnhancer())
// .userDetailsService(userServiceDetail) // 配置获取用户认证信息的接口
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtTokenEnhancer());
}
@Bean
protected JwtAccessTokenConverter jwtTokenEnhancer() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("fanfanzhisu");
return converter;
}
/**
* Token 节点的安全策略
* @param oauthServer
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients()
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
}
5. WebSecurity 配置
- 开放了actuator路径供健康检查
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userServiceDetail;
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
.and()
.authorizeRequests()
.antMatchers("/actuator/**").permitAll()
.antMatchers("/**").authenticated()
.and()
.httpBasic();
}
//
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userServiceDetail)
.passwordEncoder(new BCryptPasswordEncoder());
}
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
}
Mybatis文件配置:
配置一下model,dao等文件路径:
@Configuration
@MapperScan("com.ffzs.consulauth.**.dao")
public class MybatisConfig {
@Autowired
private DataSource dataSource;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setDataSource(dataSource);
ssfb.setTypeAliasesPackage("com.ffzs.consulauth.**.model");
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
ssfb.setMapperLocations(resolver.getResources("classpath*:**/generator/*.xml"));
return ssfb.getObject();
}
}
model,dao等文件通过idea上的generator插件生成即可。
重新写一下获取token时匹配用户部分逻辑
UserDetailsServiceImpl.class
- 管理员给ROLE_ADMIN, ROLE_USER 权限
- 普通用户只给 ROLE_USER 权限
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired(required = false)
private MyUserDao myUserDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MyUser myUser = myUserDao.findByUsername(username);
System.out.println(username);
if (myUser == null) {
throw new UsernameNotFoundException("用户不存在");
}
if (username.equals("admin") || username.equals("ffzs")){
return new User(username, myUser.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN, ROLE_USER"));
}
return new User(username, myUser.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
}
}
userdao要添加一个通过名字获取信息的方法:
启动类更改:
添加==@EnableDiscoveryClient==即可
运行&&测试
consul上注册成功
使用postman发送post请求:
http://localhost:8731/oauth/token?client_id=consul_server&client_secret=123456&grant_type=password&username=ffzs&password=123zxc
成功获取token
post请求中的参数及描述:
service配置
创建一个consul-service项目,用于提供登录,注册等服务:
1. 依赖:
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:2.2.2.RELEASE'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.2'
implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery:2.2.2.RELEASE'
implementation 'org.springframework.cloud:spring-cloud-starter-oauth2:2.2.2.RELEASE'
runtimeOnly 'mysql:mysql-connector-java'
2. 配置文件
bootstrap.yml:
spring:
application:
name: consul-service
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
feign:
httpclient:
enabled: true
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
application.yml:
server:
port: 8777
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ffzs?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
username: root
password: 123zxc
logging:
level:
root: INFO
org.springframework.web: INFO
org.springframework.security: INFO
org.springframework.security.oauth2: INFO
3. Mybatis配置
dao,model跟uaa的一样
mybatis的配置文件跟uaa的也基本相同:
@Configuration
@MapperScan("com.ffzs.consulservice.**.dao")
public class MybatisConfig {
@Autowired
private DataSource dataSource;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setDataSource(dataSource);
ssfb.setTypeAliasesPackage("com.ffzs.consulservice.**.model");
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
ssfb.setMapperLocations(resolver.getResources("classpath*:**/generator/*.xml"));
return ssfb.getObject();
}
}
4. Resource配置
ResourceConfiguration.class:
- 配置了两个测试路径,user路径用于登录,不做权限限制
/hello/admin", "/hello/header
只右ADMIN权限才能访问
@Configuration
@EnableResourceServer
public class ResourceConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/actuator/**").permitAll()
.antMatchers("/user/**").permitAll()
.antMatchers("/hello/user", "hello/test").hasRole("USER")
.antMatchers("/hello/admin", "/hello/header").hasAnyRole("ADMIN", "USER")
.antMatchers("/**").authenticated();
}
/**
* tokenServices 定义Token Service 用ResourceServerTokenservices类,配置Token是如何编码和解码的 可以用RemoteTokenServices类,即Resource Server采用远程授权服务器进行Token解码,这时也不需要配置此选项
*
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.resourceId("server") // 配置资源Id。
.tokenStore(jwtTokenStore());
}
@Bean
protected JwtAccessTokenConverter jwtTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("fanfanzhisu");
return converter;
}
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtTokenConverter());
}
}
5. 注册功能
添加一个功能用于注册,service
public int insertUser(String username, String password){
MyUser user=myUserDao.findByUsername(username);
if (user != null){
user.setPassword(encoder.encode(password));
return myUserDao.updateByPrimaryKeySelective(user);
}else{
MyUser myUser=new MyUser();
myUser.setUsername(username);
myUser.setPassword(encoder.encode(password));
return myUserDao.insertSelective(myUser);
}
}
controller:
@PostMapping("/register")
public String postUser(@RequestParam("username") String username , @RequestParam("password") String password){
int back = userServiceDetail.insertUser(username,password);
return back == 1?"注册成功":"注册失败";
}
6. 登录功能
编写一个service通过feign向注册中心的consul-auth,也就是上面uaa发送登录请求并获取token
@FeignClient(value = "consul-auth")
public interface AuthServiceClient {
@PostMapping(value = "/oauth/token")
MyToken getToken(@RequestHeader("Content-Type") String content, @RequestParam("client_id") String client_id, @RequestParam("client_secret") String client_secret, @RequestParam("grant_type") String type,
@RequestParam("username") String username, @RequestParam("password") String password);
}
调用getToken获取token,之后装入userLoginDTO中:
public UserLoginDTO login(String username, String password){
MyUser user=myUserDao.findByUsername(username);
if (null == user) {
throw new RuntimeException("error username");
}
if(!encoder.matches(password,user.getPassword())){
throw new RuntimeException("error password");
}
MyToken myToken =authServiceClient.getToken("application/json", "consul_server","123456","password", username, password);
if(myToken ==null){
throw new RuntimeException("error internal");
}
UserLoginDTO userLoginDTO=new UserLoginDTO();
userLoginDTO.setMyToken(myToken);
userLoginDTO.setUser(user);
return userLoginDTO;
}
用于登入的controller:
@PostMapping("/login")
public UserLoginDTO login(@RequestParam("username") String username , @RequestParam("password") String password){
return userServiceDetail.login(username,password);
}
7. 测试登录
测试注册功能,访问http://localhost:8777/user/register?username=xiaozhang&password=123456,结果如下:
查看数据库,xiaozhang的用户信息已经存入数据库:
测试登入功能,http://localhost:8777/user/login?username=xiaozhang&password=123456 ,返回token说明登录成功:
8. 测试权限
因为一些业务的需求会有一些端口需要鉴权,编写用于测试的controller:
@RestController
@RequestMapping("hello")
public class TestController {
@GetMapping("user")
public String user(){
return "hello!!! 普通用户 !!!";
}
@GetMapping("admin")
public String admin(){
return "hello!!! 权限dog !!!!";
}
@GetMapping("role")
public String test() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
StringBuilder res = new StringBuilder()
.append("用户名: ").append(authentication.getName()).append("\n")
.append("权限情况: ");
for (Object it :authentication.getAuthorities().toArray()) {
res.append(it).append("\t");
}
return res.toString();
}
@RequestMapping("header")
public String header(HttpServletRequest request) {
StringBuilder html = new StringBuilder("<table border='2' cellspacing='0'>");
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
html.append("<tr><td>").append(key).append("</td><td>").append(request.getHeader(key)).append("</td></tr>");
}
html.append("</table>");
return html.toString();
}
}
几个端口权限设置如下:
.antMatchers("/hello/user", "hello/test").hasRole("USER")
.antMatchers("/hello/admin", "/hello/header").hasAnyRole("ADMIN", "USER")
- USER 权限可以访问 “/hello/user”, “hello/test”
- ADMIN 都可以访问
设置token,使用postman可以直接设置,如下图,不使用postman的话可以写道header里:
这时我们访问,http://localhost:8777/hello/user,可以访问user;
访问,http://localhost:8777/hello/admin,因为没有权限,限制访问:
访问,http://localhost:8777/hello/role,这里我们可以看到xiaozhang只有ROLU_USER的权限:
更换用户,使用ffzs
账号登录,更换获得token,权限都token都长了:
使用ffzs的账号访问http://localhost:8777/hello/role
ROLE_ADMIN ROLE_USER同时拥有两个权限:
这时我们试试用这个账号能否访问http://localhost:8777/hello/admin
有了ADMIN权限可以正常访问了。
看一下header,访问 http://localhost:8777/hello/header
可见token在header里的形式,如果不容postman发送请求就添加通过将"authorization":"Bearer token"
添加到你的header中就可以访问了
来源:oschina
链接:https://my.oschina.net/u/4406280/blog/4306297