springSecurity+Oauth2.0之授权模式(客户端、密码模式)
SpringSecurity+Oauth2.0之授权模式
1: 客户端模式:
maven依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.0.6.RELEASE</version>
</dependency>
创建Oauth2.0需要创建三个相关的表,直接使用官方的SQL脚本即可生成(不要修改表名和字段名)
OAuth2 官方的项目中可以找到:https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
-- ---------------------------- -- Table structure for oauth_access_token -- ---------------------------- DROP TABLE IF EXISTS `oauth_access_token`; CREATE TABLE `oauth_access_token` ( `token_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `token` blob NULL, `authentication_id` varchar(250) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `user_name` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `client_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `authentication` blob NULL, `refresh_token` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`authentication_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for oauth_client_details -- ---------------------------- DROP TABLE IF EXISTS `oauth_client_details`; CREATE TABLE `oauth_client_details` ( `client_id` varchar(250) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `access_token_validity` int(11) NULL DEFAULT NULL, `refresh_token_validity` int(11) NULL DEFAULT NULL, `additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`client_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for oauth_refresh_token -- ---------------------------- DROP TABLE IF EXISTS `oauth_refresh_token`; CREATE TABLE `oauth_refresh_token` ( `token_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `token` blob NULL, `authentication` blob NULL ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; ————————————————
配置yml(这里没用到数据库可以不配置):
server: port: 8082 mybatis: mapper-locations: classpath*:mapper/*Mapper.xml configuration: database-id: MySQL # 开启驼峰转换 map-underscore-to-camel-case: true # spring boot集成mybatis的方式打印sql # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl spring: application: name: oauth2-security-8082 datasource: druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/my-study?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=utf8&autoReconnect=true&failOverReadOnly=false username: root password: 123456 filters: stat # 设置最大数据库连接数,设为0为无限制 maxActive: 20 # 配置初始化大小、最小、最大 initialSize: 1 # 最大等待时间 maxWait: 60000 # 始终保留在池中的最小连接数,如果连接验证失败将缩小至此值 minIdle: 1 timeBetweenEvictionRunsMillis: 6000 # 连接在池中保持空闲而不被回收的最小时间(毫秒) minEvictableIdleTimeMillis: 30000 validationQuery: select 'x' # 对池中空闲的连接是否进行验证,验证失败则回收此连接(默认为false) testWhileIdle: true # 当从连接池中取出一个连接时是否进行验证,若验证失败则从池中删除该连接并尝试取出另一个连接 testOnBorrow: true # 当一个连接使用完归还到连接池时是否进行验证 testOnReturn: false # 启用游标缓存,这个对数据库的性能提升很大 poolPreparedStatements: true # 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100 maxOpenPreparedStatements: 20 filter: stat: log-slow-sql: true slow-sql-millis: 2000
配置认证服务器:
/** * @Author dw * @ClassName AuthorizationServerConfig * @Description 配置认证服务器 提供/oauth2/authorize,/oauth2/token,/oauth2/check_token,/oauth2/confirm_access,/oauth2/error * @EnableAuthorizationServer // 这个注解告诉 Spring 这个应用是 OAuth2 的授权服务器 * @Date 2020/4/20 14:47 * @Version 1.0 */ @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private DataSource dataSource; @Bean public TokenStore tokenStore() { //使用内存中的 token store // return new InMemoryTokenStore(); //使用Jdbctoken store return new JdbcTokenStore(dataSource); } @Bean public ClientDetailsService clientDetails() { return new JdbcClientDetailsService(dataSource); } /** * @return * @Author dw * @Description 配置token的过期日期等 * @Date 2020/4/23 18:32 * @Param */ @Bean @Primary public DefaultTokenServices defaultTokenServices() { DefaultTokenServices services = new DefaultTokenServices(); //设置Token 20秒过期 services.setAccessTokenValiditySeconds(20); //设置刷新token的过期时间 services.setRefreshTokenValiditySeconds(666); services.setTokenStore(tokenStore()); return services; } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // 配置客户端, 用于client认证 clients.withClientDetails(clientDetails()); // 第一次使用的时候,需要配置客户端信息,或者手动添加客户端信息到数据库oauth_client_details表中 // clients.jdbc(dataSource) // .withClient("myClient") // .secret(new BCryptPasswordEncoder().encode("123456")) // .authorizedGrantTypes("password", "refresh_token")//允许授权范围 // .authorities("ROLE_ADMIN","ROLE_USER")//客户端可以使用的权限 // .scopes( "read", "write") // .accessTokenValiditySeconds(7200) // .refreshTokenValiditySeconds(7200); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { // endpoints.tokenStore(tokenStore()) endpoints.tokenServices(defaultTokenServices()) //接收GET和POST .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security .tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()") .allowFormAuthenticationForClients();//允许表单登录 } }
如果说是其他模式:
.authorizedGrantTypes("authorization_code","client_credentials","password","refresh_token")//授权方式
"authorization_code" : code模式
"client_credentials": 客户端模式
"password": 密码模式
"implicit": 简化模式
关于设置Token的过期时间如果配置的是从数据库读取,上面配置的Token过期时间将无效,可以在数据库中设置过期时间
配置资源服务器:
/** * @Author dw * @ClassName ResourceServerConfig * @Description 配置资源器 * @EnableResourceServer 这个类表明了此应用是OAuth2 的资源服务器,此处主要指定了受资源服务器保护的资源链接 * @Date 2020/4/20 15:03 * @Version 1.0 */ @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable() //匹配需要资源认证路径 .antMatcher("/**") .authorizeRequests() //匹配不需要资源认证路径 .antMatchers("/oauth/**").permitAll() .anyRequest().authenticated(); } }
配置springSecurity:
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { // 配置密码的加密方式 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 用来配置拦截保护的请求 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() //匹配需要资源认证路径 .antMatcher("/**") .authorizeRequests() //匹配不需要资源认证路径 .antMatchers("/oauth/**").permitAll() .anyRequest().authenticated(); } public static void main(String[] args) { System.out.println(new BCryptPasswordEncoder().encode("123456")); } }
Controller受保护的资源:
/** * @Author dw * @ClassName HelloSecurityController * @Description * @Date 2020/4/14 11:34 * @Version 1.0 */ @RestController public class HelloSecurityController { @GetMapping("/admin/hello") public String admin() { return "hello admin"; } @GetMapping("/user/hello") public String user() { return "hello user"; } @GetMapping("/db/hello") public String db() { return "hello db"; } @GetMapping("/hello") public String hello() { return "hello"; } }
注意:这里可能需要你手动配置你的客户端到这张表中
获取token:
- grant_type:表示授权类型,必选项,此处的值固定为"client_credentials"
- client_id:表示客户端的ID,必选项
- client_secret:客户端的密码,可选项
- scope:表示申请的权限范围,可选项
通过Token访问受保护的资源:
2: 密码模式:
这里分成资源服务器与授权服务器,即分离的形式来搭建,而不是资源服务器与授权服务器都在同一个项目中
maven依赖(两个项目中都要添加):
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
<dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.0.6.RELEASE</version> </dependency>
其他的依赖,可能项目中用得上:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--操作数据库--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- Druid 数据库连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.22</version> </dependency> <!-- region MyBatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
2.1:首先我们先搭建授权服务:
yml配置:
server: port: 8086 mybatis: mapper-locations: classpath*:mapper/*Mapper.xml configuration: database-id: MySQL # 开启驼峰转换 map-underscore-to-camel-case: true # spring boot集成mybatis的方式打印sql # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl spring: application: name: oauth2-security-password-8086 datasource: druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/my-study?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=utf8&autoReconnect=true&failOverReadOnly=false username: root password: 123456 filters: stat # 设置最大数据库连接数,设为0为无限制 maxActive: 20 # 配置初始化大小、最小、最大 initialSize: 1 # 最大等待时间 maxWait: 60000 # 始终保留在池中的最小连接数,如果连接验证失败将缩小至此值 minIdle: 1 timeBetweenEvictionRunsMillis: 6000 # 连接在池中保持空闲而不被回收的最小时间(毫秒) minEvictableIdleTimeMillis: 30000 validationQuery: select 'x' # 对池中空闲的连接是否进行验证,验证失败则回收此连接(默认为false) testWhileIdle: true # 当从连接池中取出一个连接时是否进行验证,若验证失败则从池中删除该连接并尝试取出另一个连接 testOnBorrow: true # 当一个连接使用完归还到连接池时是否进行验证 testOnReturn: false # 启用游标缓存,这个对数据库的性能提升很大 poolPreparedStatements: true # 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100 maxOpenPreparedStatements: 20 filter: stat: log-slow-sql: true slow-sql-millis: 2000
创建表:用户、角色、权限、用户角色表、角色权限表。
CREATE TABLE `user` ( `id` int(64) NOT NULL AUTO_INCREMENT, `user_name` varchar(32) DEFAULT NULL, `password` varchar(255) DEFAULT NULL, `enable` tinyint(4) DEFAULT NULL, `locked` tinyint(4) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; CREATE TABLE `role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(32) DEFAULT NULL, `description` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; CREATE TABLE `resources` ( `id` int(11) NOT NULL AUTO_INCREMENT, `pattern` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; CREATE TABLE `user_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) DEFAULT NULL, `role_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8; CREATE TABLE `role_resource` ( `id` int(11) NOT NULL AUTO_INCREMENT, `role_id` int(11) DEFAULT NULL, `resource_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
创建实体对象:
package com.dw.study.entity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * (User)实体类 * * @author makejava * @since 2020-04-14 13:10:12 */ public class User implements UserDetails, Serializable { private static final long serialVersionUID = 7673225091735750618L; private Integer id; private String userName; private String password; private boolean enable; private boolean locked; private List<Role> userRoles; @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<SimpleGrantedAuthority> authorities = new ArrayList<>(); for (Role role : userRoles) { authorities.add(new SimpleGrantedAuthority(role.getName())); } return authorities; } @Override public String getUsername() { return userName; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return !locked; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return enable; } public void setPassword(String password) { this.password = password; } @Override public String getPassword() { return password; } public boolean isEnable() { return enable; } public void setEnable(boolean enable) { this.enable = enable; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public boolean isLocked() { return locked; } public void setLocked(boolean locked) { this.locked = locked; } public List<Role> getUserRoles() { return userRoles; } public void setUserRoles(List<Role> userRoles) { this.userRoles = userRoles; } }
public class Role implements Serializable { private static final long serialVersionUID = 825384782616737527L; private Integer id; private String name; private String description; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } }
public class Resources implements Serializable { private static final long serialVersionUID = -5840903661920488430L; private Integer id; private String pattern; private List<Role> roles; public List<Role> getRoles() { return roles; } public void setRoles(List<Role> roles) { this.roles = roles; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getPattern() { return pattern; } public void setPattern(String pattern) { this.pattern = pattern; } }
public class UserRole implements Serializable { private static final long serialVersionUID = -33173102844662867L; private Integer id; private Integer userId; private Integer roleId; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } public Integer getRoleId() { return roleId; } public void setRoleId(Integer roleId) { this.roleId = roleId; } }
创建dao
@Repository public interface UserMapperDao { public User loadUserByUsername(String userName); public List<Role> getUserRolesByUid(Integer id); }
创建mapper
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.dw.study.dao.ResourceMapperDao"> <resultMap id="ResourcesMap" type="com.dw.study.entity.Resources"> <id column="id" property="id"/> <result column="pattern" property="pattern"/> <collection property="roles" ofType="com.dw.study.entity.Role"> <id column="roleId" property="id"/> <result column="name" property="name"/> <result column="description" property="description"/> </collection> </resultMap> <select id="getAllResources" resultMap="ResourcesMap"> SELECT r.*, re.id AS roleId, re.`name`, re.description FROM resources AS r LEFT JOIN role_resource AS rr ON r.id = rr.resource_id LEFT JOIN role AS re ON re.id = rr.role_id </select> </mapper>
创建service
@Service public class MyUserDetailServiceImpl implements UserDetailsService { @Autowired private UserMapperDao userMapperDao; @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapperDao.loadUserByUsername(username); String encodePassword = passwordEncoder.encode(user.getPassword()); System.out.println("加密后的密码:" + encodePassword); user.setPassword(encodePassword); if (user == null) { throw new UsernameNotFoundException("账户不存在!"); } List<Role> userRoles = userMapperDao.getUserRolesByUid(user.getId()); user.setUserRoles(userRoles); return user; } }
核心:配置认证服务器认证类
package com.dw.study.oauth2Config; import com.dw.study.service.MyUserDetailServiceImpl; import com.dw.study.webConfig.CustomWebResponseExceptionTranslator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; import org.springframework.security.oauth2.provider.token.DefaultTokenServices; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider; import javax.sql.DataSource; /** * @Author dw * @ClassName AuthorizationServerConfig * @Description 配置认证服务器 提供/oauth2/authorize,/oauth2/token,/oauth2/check_token,/oauth2/confirm_access,/oauth2/error * @EnableAuthorizationServer // 这个注解告诉 Spring 这个应用是 OAuth2 的授权服务器 * @Date 2020/4/20 14:47 * @Version 1.0 */ @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private DataSource dataSource; // 认证管理器,认证用户信息 @Autowired private AuthenticationManager authenticationManager; @Autowired private MyUserDetailServiceImpl myUserDetailService; @Autowired private CustomWebResponseExceptionTranslator webResponseExceptionTranslator; @Bean public TokenStore tokenStore() { //使用内存中的 token store // return new InMemoryTokenStore(); //使用Jdbctoken store return new JdbcTokenStore(dataSource); } @Bean public ClientDetailsService clientDetails() { return new JdbcClientDetailsService(dataSource); } /** * @return * @Author dw * @Description 配置token的过期日期等 * @Date 2020/4/23 18:32 * @Param */ // @Bean // @Primary // public DefaultTokenServices defaultTokenServices() { // DefaultTokenServices services = new DefaultTokenServices(); // // access token有效期2个小时 // services.setAccessTokenValiditySeconds(60 * 60 * 2); // // refresh token有效期30天 // services.setRefreshTokenValiditySeconds(60 * 60 * 24 * 30); // // 支持使用refresh token刷新access token // services.setSupportRefreshToken(true); // // 允许重复使用refresh token // services.setReuseRefreshToken(true); //// services.setAuthenticationManager(authenticationManager); // services.setTokenStore(tokenStore()); // return services; // } private PreAuthenticatedAuthenticationProvider preAuthenticatedAuthenticationProvider() { PreAuthenticatedAuthenticationProvider authenticationProvider = new PreAuthenticatedAuthenticationProvider(); authenticationProvider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper(myUserDetailService)); return authenticationProvider; } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // 配置客户端, 用于client认证 clients.withClientDetails(clientDetails()); // 第一次使用的时候,需要配置客户端信息,或者手动添加客户端信息到数据库oauth_client_details表中 // clients.jdbc(dataSource) // .withClient("myClient") // .secret(new BCryptPasswordEncoder().encode("123456")) // .authorizedGrantTypes("password", "refresh_token")//允许授权范围 // .authorities("ROLE_ADMIN","ROLE_USER")//客户端可以使用的权限 // .scopes( "read", "write") // .accessTokenValiditySeconds(7200) // .refreshTokenValiditySeconds(7200); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore()) // endpoints.tokenServices(defaultTokenServices()) .authenticationManager(authenticationManager) .userDetailsService(myUserDetailService) //接收GET和POST .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST) .exceptionTranslator(webResponseExceptionTranslator); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security .tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()") .allowFormAuthenticationForClients();//允许表单登录 } }
如果不配置token的过期时间,默认是12个小时过期
配置资源
package com.dw.study.oauth2Config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; /** * @Author dw * @ClassName ResourceServerConfig * @Description 配置资源器 * @EnableResourceServer 这个类表明了此应用是OAuth2 的资源服务器,此处主要指定了受资源服务器保护的资源链接 * @Date 2020/4/20 15:03 * @Version 1.0 */ @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http //定义哪些url需要被保护 哪些不需要保护 .authorizeRequests() //定义这两个链接不需要登录可访问 .antMatchers("/oauth/token" , "oauth/check_token").permitAll() //定义所有的都不需要登录 目前是测试需要 // .antMatchers("/**").permitAll() .anyRequest().authenticated() //其他的都需要登录 //.antMatchers("/sys/**").hasRole("admin")///sys/**下的请求 需要有admin的角色 .and() .formLogin() // .loginPage("/login") //如果未登录则跳转登录的页面 这儿可以控制登录成功和登录失败跳转的页面 .and() .csrf().disable();//防止跨站请求 spring security中默认开启 } }
配置springSecurity
package com.dw.study.securityConfig; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * @Author dw * @ClassName WebSecurityConfig * @Description * @Date 2020/4/20 15:07 * @Version 1.0 */ @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean() ; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 配置用户签名服务 主要是user-details 机制, * * @param auth 签名管理器构造器,用于构建用户具体权限控制,这里交给oauth2的AuthorizationServer去处理 * @throws Exception */ // @Override // protected void configure(AuthenticationManagerBuilder auth) throws Exception { // auth.userDetailsService(myUserService) // .passwordEncoder(passwordEncoder()); // } /** * 用来配置拦截保护的请求 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http //定义哪些url需要被保护 哪些不需要保护 .authorizeRequests() //定义这两个链接不需要登录可访问 .antMatchers("/oauth/token" , "oauth/check_token").permitAll() //定义所有的都不需要登录 目前是测试需要 // .antMatchers("/**").permitAll() .anyRequest().authenticated() //其他的都需要登录 //.antMatchers("/sys/**").hasRole("admin")///sys/**下的请求 需要有admin的角色 .and() .formLogin() // .loginPage("/login") //如果未登录则跳转登录的页面 这儿可以控制登录成功和登录失败跳转的页面 .and() .csrf().disable();//防止跨站请求 spring security中默认开启 } }
配置全局异常处理类:
package com.dw.study.webConfig; import org.bouncycastle.asn1.ocsp.ResponseData; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator; import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator; import org.springframework.stereotype.Component; /** * @Author dw * @Description web全局异常返回处理器 * @Date 2020/4/24 14:14 * @Param * @return */ @Component public class CustomWebResponseExceptionTranslator extends DefaultWebResponseExceptionTranslator { private static final Logger LOGGER = LoggerFactory.getLogger(CustomWebResponseExceptionTranslator.class); @SuppressWarnings("PMD") @Override
public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
ResponseEntity responseEntity = super.translate(e);
LOGGER.info("statusCode: " + responseEntity.getStatusCodeValue());
OAuth2Exception oAuth2Exception = new OAuth2Exception("出错了!!");
if(responseEntity.getStatusCodeValue() == 401){
oAuth2Exception.addAdditionalInformation("code", "401");
oAuth2Exception.addAdditionalInformation("data", "");
oAuth2Exception.addAdditionalInformation("message", "用户名错误!");
return new ResponseEntity(oAuth2Exception, HttpStatus.UNAUTHORIZED);
}else if(responseEntity.getStatusCodeValue() == 400){
oAuth2Exception.addAdditionalInformation("code", "400");
oAuth2Exception.addAdditionalInformation("data", "");
oAuth2Exception.addAdditionalInformation("message", "用户密码错误!");
return new ResponseEntity(oAuth2Exception, HttpStatus.BAD_REQUEST);
}else {
oAuth2Exception.addAdditionalInformation("code", String.valueOf(responseEntity.getStatusCodeValue()));
oAuth2Exception.addAdditionalInformation("data", "");
oAuth2Exception.addAdditionalInformation("message", "认证异常");
return new ResponseEntity(oAuth2Exception, HttpStatus.UNAUTHORIZED);
}
}
}
创建访问当前登录用户的接口:
package com.dw.study.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.security.Principal; /** * @Author dw * @ClassName UserInfoController * @Description * @Date 2020/4/23 15:06 * @Version 1.0 */ @RestController public class UserInfoController { @Value("${server.port}") private String serverPort; @GetMapping(value = "/userInfo") public Principal getUserInfo(Principal principal) { System.out.println("通过端口:" + serverPort + "获取用户信息:" + principal); return principal; } }
好的到这里认证服务器就配置完了。
下面配置资源服务器:
1、pom同上;
修改yml:
server:
port: 8087
management:
security:
enabled: false
security: oauth2: #token检查的授权端url authorization: check-token-access: http://127.0.0.1:8086/oauth/check_token #对应的注册码与注册密码 client: id: oauth2-password client-secret: 123456 userAuthorizationUri: http://127.0.0.1:8086/oauth/authorize access-token-uri: http://127.0.0.1:8086/oauth/token scope: all grant-type: password # authentication-scheme: form #获得授权端的当前用户信息url resource: user-info-uri: http://127.0.0.1:8086/userInfo
定义资源服务器配置:
package com.dw.study.oauth2Config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.web.cors.CorsConfiguration; import javax.servlet.http.HttpServletResponse; /** * @Author dw * @ClassName ResourceServerConfig * @Description 配置资源器 * @EnableResourceServer 这个类表明了此应用是OAuth2 的资源服务器,此处主要指定了受资源服务器保护的资源链接 * @EnableGlobalMethodSecurity : 开启三种可以在方法上面加权限控制的注解 * @Date 2020/4/20 15:03 * @Version 1.0 */ @Configuration @EnableResourceServer @EnableOAuth2Client @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable() //匹配需要资源认证路径 .antMatcher("/**") .authorizeRequests() //匹配不需要资源认证路径 .antMatchers("/oauth/**").permitAll() .anyRequest().authenticated(); } }
定义获取用户信息的util:
package com.dw.study.Utils; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.provider.OAuth2Authentication; import java.util.LinkedHashMap; /** * @Author dw * @ClassName UserUtil * @Description * @Date 2020/4/24 17:05 * @Version 1.0 */ public class UserUtil { private UserUtil (){} public static LinkedHashMap getUser(){ return getUserDetails(); } private static LinkedHashMap getUserDetails() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); OAuth2Authentication oAuth2Authentication = (OAuth2Authentication)authentication; LinkedHashMap userDetails = (LinkedHashMap) oAuth2Authentication.getUserAuthentication().getDetails(); LinkedHashMap principal = (LinkedHashMap) userDetails.get("principal"); // 这里需要手动传换成User对象 return principal; } }
最后新建user、role类,同上面一样
建controller
@RestController @Slf4j public class MyController { @Value("${server.port}") private String serverPort; @GetMapping("/get") public String get(){ return "成功获取到资源: port: " + serverPort; } @GetMapping("/userInfo") public Object getUserInfo(){ LinkedHashMap user = UserUtil.getUser(); return user; } }
好的,配置完成。下面测试
1:oauth_client_detais这张表中配置我们的客户端信息
启动两个项目,然后使用postman:
1: 获取token:
2:刷新token获取token:
访问用户信息:
关于使用HTTP请求头 Authorization,不直接用 client_id 和 client_secret
客户端发送http请求流程:
服务器发现配置了http auth,于是检查request里面有没有"Authorization"的http header
如果有,则判断Authorization里面的内容是否在用户列表里面,Authorization header 的典型数据为"Authorization: Basic jdhaHY0=",
其中Basic 表示基础认证,
jdhaHY0= 是base64编码的"user:passwd"字符串。
如果没有,或者用户密码不对,则返回http code 401页面给客户端。
base64在线编码j解码工具:http://tool.chinaz.com/Tools/Base64.aspx
Header:
Authorization : Basic YnJvd3Nlcjo=
配置跨域访问问题:
/** * 跨域访问配置 */ @Configuration @Order(Ordered.HIGHEST_PRECEDENCE) public class CORSFilter implements Filter { public static final String CREDENTIALS_NAME = "Access-Control-Allow-Credentials"; public static final String ORIGIN_NAME = "Access-Control-Allow-Origin"; public static final String METHODS_NAME = "Access-Control-Allow-Methods"; public static final String HEADERS_NAME = "Access-Control-Allow-Headers"; public static final String MAX_AGE_NAME = "Access-Control-Max-Age"; @Override public void destroy() { } @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) resp; HttpServletRequest request = (HttpServletRequest) req; response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN"); if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); } else { chain.doFilter(req, resp); } } @Override public void init(FilterConfig filterConfig) throws ServletException { } }