Spring Security oAuth2
Spring Security
Spring Security是基于javaEE实现的企业级认证授权框架,底层采用Filter链实现认证授权。和spring boot整合后简易使用。
Spring Security认证流程
1. 用户提交用户名密码
2. 经过UsernamePasswordAuthenticationFilter认证过滤器,交给AuthenticationManager认证。
3. AuthenticationManager交给AuthenticationProvider(DaoAuthenticationProvider)认证
4. AuthenticationProvider通过UserDetailsService的loadUserByUsername()方法获取用户信息UserDetails
5. 通过PasswordEncoder对比输入的密码和UserDetails的密码是否一致,给Authentication增加认证信息
6. 将Authentication认证信息通过SecurityContextHolder.getContext().setAuthentication()方法保存到上下文
因此,可以写一个UserDetailsService的实现类,将用户的身份信息放入。
Spring Security授权流程
通过SecurityConfig继承WebSecurityConfigurerAdapter,实现configure(HttpSecurity http),可以用http.authorizeRequests()
对web请求进行授权保护,原理为采用标准的Filter对web请求拦截,实现资源授权访问
- 用户访问受保护资源,要经过FilterSecurityInterceptor
- 然后通过SecurityMetadataSource.getAttributes()获取当前资源所需的权限,返回Collection<ConfigAttribute>
- AccessDecisonManager.Decide()投票决策是否通过,允许访问
web授权(基于url)
web授权尽量采用继续权限的授权而不是基于角色。路径范围大的放最后(和拦截器相同)避免出现问题。
http.csrf().disable()
.authorizeRequests()
.antMatchers("/level1/").hasRole(“VIP1”)
.antMatchers("/level2/").hasRole(“VIP2”)
.antMatchers("/level3/").hasRole(“VIP3”)
.antMatchers("/").permitAll();
方法授权(基于注解)
SecurityConfig配置类添加@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)开启。
方法上有@PreAuthorize、@PostAuthorize、@Secured (不常用)三种
@PreAuthorize("hasRole('VIP1')")
@GetMapping("/say")
public String say(){
System.out.println("say");
return "ok";
}
@PreAuthorize("hasRole('VIP1') and hasRole('VIP2')")
@GetMapping("/sayHi")
public String sayHi(){
System.out.println("say hi");
return "ok";
}
Spring Security会话
Spring Security登录后,信息存储在SecurityContextHolder中,可以从中获取UserDetails对象等
public String getUsername() {
String username = null;
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();
// 未认证
if (principal == null) {
username = "匿名";
} else {
if (principal instanceof UserDetails) {
UserDetails userDetails = (UserDetails) principal;
username = userDetails.getUsername();
} else {
username = principal.toString();
}
}
return username;
}
会话存在四种状态,可以通过http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)来设置
1. always 没有session就创建一个
2. ifRequired 需要时创建
3. never 不创建,如果有就使用
4. stateless 不创建,也不使用 分布式系统使用无状态令牌token时,不需要session
Spring Security 实现
1. 导入spring-boot-starter-security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2. 添加配置类SecurityConfig,继承WebSecurityConfigurerAdapter,内部有密码编译器规则,用户信息认证,授权
2.1 基于内存实现
package com.shyb.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* @EnableWebSecurity 开启SpringSecurity 改注解带了@Configuration 会被扫描到
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//定制授权规则 路径范围大的放最后拦截
// 1. csrf().disable()关闭自定义页面有CSRF跨站请求伪造的发生,限制除GET以外大多数请求
// 2. 也可以采用增加隐藏token也解决CSRF问题
http.csrf().disable()
.authorizeRequests()
.antMatchers("/level1/**").hasRole("VIP1")
.antMatchers("/level2/**").hasRole("VIP2")
.antMatchers("/level3/**").hasRole("VIP3")
.antMatchers("/**").permitAll();
//开启自动配置的登录功能 没有登陆会到登录页面,登录成功后跳转到输入的网页处
//1. 默认到/login来到登录页
//2. 登录失败重定向到/login?error页面
// 自定义login页面 loginPage
// 默认/login Get请求是到登录页面 Post是登录 默认账户密码为username和password,
//可以usernameParameter("mobile").passwordParameter("pwd");指定参数
http.formLogin().loginPage("/login").usernameParameter("mobile").passwordParameter("pwd");
// 开启自己注销功能
// 1. 访问/logout标识用户自动注销
// 2. 规定是post请求,所以需要form表单指定method
// 3. 注销后会跳转到/login?logout页面
// logoutSuccessUrl主要成功后的页面
http.logout().logoutSuccessUrl("/");
// 开启记住我功能
// 登录成功后,将Cookie发给浏览器保存,以后访问会带上这个Cookie,通过检查就可以免登陆
// 点击注销会删除Cookie 默认保存时间是14天
// 自定义记住我时,使用rememberMeParameter("remember")来接受参数
http.rememberMe().rememberMeParameter("remember");
}
/**
* 密码编译器
* @return
*/
@Bean
PasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 用户信息认证(内存)
* @return
*/
@Override
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password(bCryptPasswordEncoder().encode("123456")).roles("VIP1","VIP2").build());
manager.createUser(User.withUsername("lisi").password(bCryptPasswordEncoder().encode("123456")).roles("VIP2","VIP3").build());
manager.createUser(User.withUsername("wangwu").password(bCryptPasswordEncoder().encode("123456")).authorities("create","p1").build());
return manager;
}
}
2.2 基于数据库实现(写UserDetailsServiceImpl实现类实现UserDetailsService,实现loadUserByUsername方法),上面
SecurityConfig的userDetailsService()删除
package com.shyb.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
/**
* @author wzh
* @date 2019/12/19 - 8:24
*/
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
PasswordEncoder bCryptPasswordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
System.out.println("username:"+s);
//模拟数据库查询 正常时注入UserMapper,根据用户名查询用户
//查询为null时,直接返回null即可
//查询不为null,调用下面方法将账号、密码、权限、身份设置进UserDetails,security会去检验密码和权限
UserDetails userDetails = User.withUsername("zhangsan")
.password(bCryptPasswordEncoder.encode("123456")).roles("VIP1","VIP3").build();
return userDetails;
}
}
分布式服务认证
1. 随着项目的复杂度上升,分布式服务越来越多,每个服务都部署认证太过冗余。
2. 第三方服务访问,也需要授权(类似微信、支付宝)
3. 认证方式必须可扩展(账号密码、短信验证码、二维码、人脸识别等)。
因此,可以单独部署认证服务器,所有认证服务都需请求认证服务器,OAuth2协议就能解决上面问题。
OAuth2简介
oAuth 协议是为用户资源的授权的标准。允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要
将账号和密码提供给第三方应用或分享他们数据的所有内容。
OAuth2环境介绍
- 采用springcloud微服务,依赖spring-cloud-starter-oauth2
- OAuth2提供TokenEndpoint访问令牌请求。默认URL: /oauth/token
- AuthorizationEndpoint服务于认证请求。默认URL: /oauth/authorize
- 资源服务器要校验token的合法性,使用的是OAuth2AuthenticationProcessingFilter
OAuth2使用
OAuth2要在Spring Security认证基础上,增加授权服务器配置
- 在config包下增加AuthorizationServerConfig类,继承AuthorizationServerConfigurerAdapter,并增加@EnableAuthorizationServer
- OAuth2客户端授权方式有四种
- 简化模式(不常用)
- 授权码模式(适用第三方接入)
授权码模式用户确认后,认证服务器会返回code,第三方服务通过code、client_id、client_secret请求认证服务器获得access_token和refresh_token。code只能使用一次。 - 密码模式(适用内部系统认证)
密码模式会泄露密码,因此不能用于接第三方系统。 - 客户端模式(不常用)
- 实现AuthorizationServerConfigurerAdapter的三个方法,默认token生成策略为内存模式,可以通过注入bean修改。密码模式需要在WebSecurityConfig中将父类authenticationManager()重写注入spring。授权码模式需要注入AuthorizationCodeServices
- 令牌访问与刷新
- Access Token 是客户端访问资源服务器的令牌。这个授权应该是 临时 的,有一定
有效期。因为,Access Token 在使用的过程中 可能会泄露。然而引入了有效期之后,
每当 Access Token 过期,客户端就必须重新向用户索要授权。非常影响用户体验。
oAuth2.0 引入了 Refresh Token 机制 - Refresh Token 的作用是用来刷新 Access Token。认证服务器提供一个刷新接口。
Refresh Token 一定是保存在客户端的服务器上 。oAuth2.0 引入了 client_secret
机制。客户端必须把 client_secret 妥善保管在服务器上,刷新 Access Token 时,
需要验证这个 client_secret。
- Access Token 是客户端访问资源服务器的令牌。这个授权应该是 临时 的,有一定
授权码模式请求流程
1. 访问获取授权码
ip:port/oauth/authorize?client_id=client&response_type=code
2. 授权页面通过,地址栏会增加参数code=MesLkD
3. PostMan请求/oauth/token,Post请求,参数有client_id,client_secret,grant_type=authorization_code,code=MesLkD
基于内存实现
application.yml
spring:
application:
name: oauth2-server
server:
port: 8080
授权服务器
@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
BCryptPasswordEncoder passwordEncoder;
@Autowired
ClientDetailsService clientDetailsService;
@Autowired
AuthenticationManager authenticationManager;
//令牌生成方案 默认在内存中生成普通令牌
@Bean
public TokenStore tokenStore(){
return new InMemoryTokenStore();
}
// 配置客户端详情信息服务 客户端通过client_id和client_secret来访问资源
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client")// client_id
.secret(passwordEncoder.encode("secret"))// client_secret
.authorizedGrantTypes("authorization_code")// 授权类型,共有authorization_code,password,client_credentials,implicit,refresh_token五种
.scopes("app")// 授权范围
.autoApprove(false)// false跳转到授权页面 true 直接发令牌
.redirectUris("http://www.baidu.com");// 注册回调地址
}
// 令牌访问服务
@Bean
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setClientDetailsService(clientDetailsService);//客户端信息服务
tokenServices.setTokenStore(tokenStore());// 令牌生成方案
tokenServices.setRefreshTokenValiditySeconds(7200);//令牌默认有效期2小时
tokenServices.setAccessTokenValiditySeconds(259200);// 刷新令牌默认有效期3天
tokenServices.setSupportRefreshToken(true);// 设置刷新令牌
return tokenServices;
}
// 授权码模式
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
return new InMemoryAuthorizationCodeServices();
}
// 令牌访问端点
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)//密码模式需要
.authorizationCodeServices(authorizationCodeServices())//授权码模式需要
.tokenStore(tokenStore())//令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST);//允许POST提交
}
// 令牌访问策略
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()") //oauth/token_key公开
.checkTokenAccess("permitAll()") //oauth/check_token公开
.allowFormAuthenticationForClients();//允许表单认证,申请令牌
}
}
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
PasswordEncoder bCryptPasswordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//实际UserDetails根据s用户名去数据库查询,将结果放入UserDetails中,系统会去比较
System.out.println("username:"+s);
UserDetails userDetails = User.withUsername("zhangsan")
.password(bCryptPasswordEncoder.encode("123456")).roles("VIP1","VIP3").build();
return userDetails;
}
}
基于jdbc存储token
和基于内存的区别:
- 增加DataSource的配置
- 令牌存储方案TokenStore 修改为JdbcTokenStore
- 客户端读取设置ClientDetailsService修改为JdbcClientDetailsService,将上面ClientDetails的配置写入oauth_client_details表中
- 令牌访问端点的令牌服务管理设置(已写)
- 配置客户端信息ClientDetailsServiceConfigurer,使用withClientDetails
默认URL - 添加spring-boot-starter-jdbc和mysql-connector-java的依赖
/oauth/authorize:授权端点
/oauth/token:令牌端点
/oauth/confirm_access:用户确认授权提交端点
/oauth/error:授权服务错误信息端点
/oauth/check_token:用于资源服务访问的令牌解析端点
/oauth/token_key:提供公有密匙的端点,如果你使用 JWT 令牌的话
oauth2数据库sql
application.yml
spring:
application:
name: oauth2-server
datasource:
type: com.zaxxer.hikari.HikariDataSource
username: root
password: yali
url: jdbc:mysql://localhost:3306/oauth2?characterEncoding=utf-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&useSSL=true&zeroDateTimeBehavior=convertToNull&autoReconnect = true
hikari:
maximum-pool-size: 20
max-lifetime: 30000
idle-timeout: 30000
data-source-properties:
prepStmtCacheSize: 250
prepStmtCacheSqlLimit: 2048
cachePrepStmts: true
useServerPrepStmts: true
server:
port: 8080
@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
BCryptPasswordEncoder passwordEncoder;
@Autowired
AuthenticationManager authenticationManager;
@Autowired
HikariDataSource hikariDataSource;
//令牌生成方案 默认在内存中生成普通令牌
@Bean
public TokenStore tokenStore(){
return new JdbcTokenStore(hikariDataSource);
}
public ClientDetailsService jdbcClientDetailsService(){
return new JdbcClientDetailsService(hikariDataSource);
}
// 配置客户端详情信息服务 客户端通过client_id和client_secret来访问资源
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(jdbcClientDetailsService());
}
// 令牌访问服务
@Bean
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setClientDetailsService(jdbcClientDetailsService());//客户端信息服务
tokenServices.setTokenStore(tokenStore());// 令牌生成方案
tokenServices.setRefreshTokenValiditySeconds(7200);//令牌默认有效期2小时
tokenServices.setAccessTokenValiditySeconds(259200);// 刷新令牌默认有效期3天
tokenServices.setSupportRefreshToken(true);// 设置刷新令牌
return tokenServices;
}
// 授权码模式
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
return new JdbcAuthorizationCodeServices(hikariDataSource);
}
// 令牌访问端点
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)//密码模式需要
.authorizationCodeServices(authorizationCodeServices())//授权码模式需要
.tokenStore(tokenStore())//令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST);//允许POST提交
}
// 令牌访问策略
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()") //oauth/token_key公开
.checkTokenAccess("permitAll()") //oauth/check_token公开
.allowFormAuthenticationForClients();//允许表单认证,申请令牌
}
}
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
PasswordEncoder bCryptPasswordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//实际UserDetails根据s用户名去数据库查询,将结果放入UserDetails中,系统会去比较 参考下面RBAC
System.out.println("username:"+s);
UserDetails userDetails = User.withUsername("zhangsan")
.password(bCryptPasswordEncoder.encode("123456")).roles("VIP1","VIP3").build();
return userDetails;
}
}
RBAC基于角色的权限控制
在基于JDBC的基础上,增加RBAC角色控制
- 引入mybatis-spring-boot-starter
- 引入mapper-spring-boot-starter(使用tk.mybatis)
- 执行下面sql脚本
/*!40101 SET NAMES utf8 */;
/*!40101 SET SQL_MODE=''*/;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`oauth2` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `oauth2`;
/*Table structure for table `tb_permission` */
DROP TABLE IF EXISTS `tb_permission`;
CREATE TABLE `tb_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) DEFAULT NULL COMMENT '父权限',
`name` varchar(64) NOT NULL COMMENT '权限名称',
`enname` varchar(64) NOT NULL COMMENT '权限英文名称',
`url` varchar(255) NOT NULL COMMENT '授权路径',
`description` varchar(200) DEFAULT NULL COMMENT '备注',
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8 COMMENT='权限表';
/*Data for the table `tb_permission` */
insert into `tb_permission`(`id`,`parent_id`,`name`,`enname`,`url`,`description`,`created`,`updated`) values
(1,0,'系统管理','System','/',NULL,'2019-12-24 12:45:33','2019-12-24 12:45:33'),
(2,1,'用户管理','SystemUser','/users/',NULL,'2019-12-24 12:45:33','2019-12-24 12:45:33'),
(3,2,'查看用户','SystemUserView','',NULL,'2019-12-24 12:45:33','2019-12-24 12:45:33'),
(4,2,'新增用户','SystemUserInsert','',NULL,'2019-12-24 12:45:33','2019-12-24 12:45:33'),
(5,2,'编辑用户','SystemUserUpdate','',NULL,'2019-12-24 12:45:33','2019-12-24 12:45:33'),
(6,2,'删除用户','SystemUserDelete','',NULL,'2019-12-24 12:45:33','2019-12-24 12:45:33');
/*Table structure for table `tb_role` */
DROP TABLE IF EXISTS `tb_role`;
CREATE TABLE `tb_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) DEFAULT NULL COMMENT '父角色',
`name` varchar(64) NOT NULL COMMENT '角色名称',
`enname` varchar(64) NOT NULL COMMENT '角色英文名称',
`description` varchar(200) DEFAULT NULL COMMENT '备注',
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='角色表';
/*Data for the table `tb_role` */
insert into `tb_role`(`id`,`parent_id`,`name`,`enname`,`description`,`created`,`updated`) values
(1,0,'超级管理员','admin',NULL,'2019-12-24 12:44:58','2019-12-24 12:45:00');
/*Table structure for table `tb_role_permission` */
DROP TABLE IF EXISTS `tb_role_permission`;
CREATE TABLE `tb_role_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_id` bigint(20) NOT NULL COMMENT '角色 ID',
`permission_id` bigint(20) NOT NULL COMMENT '权限 ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8 COMMENT='角色权限表';
/*Data for the table `tb_role_permission` */
insert into `tb_role_permission`(`id`,`role_id`,`permission_id`) values
(1,1,1),
(2,1,2),
(3,1,3),
(4,1,4),
(5,1,5),
(6,1,6);
/*Table structure for table `tb_user` */
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(64) NOT NULL COMMENT '密码,加密存储',
`phone` varchar(20) DEFAULT NULL COMMENT '注册手机号',
`email` varchar(50) DEFAULT NULL COMMENT '注册邮箱',
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`) USING BTREE,
UNIQUE KEY `phone` (`phone`) USING BTREE,
UNIQUE KEY `email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户表';
/*Data for the table `tb_user` */
insert into `tb_user`(`id`,`username`,`password`,`phone`,`email`,`created`,`updated`) values
(1,'admin','$2a$10$9ZhDOBp.sRKat4l14ygu/.LscxrMUcDAfeVOEPiYwbcRkoB09gCmi','15888888888','aaaa@qq.com','2019-12-24 12:44:47','2019-12-24 12:44:49');
/*Table structure for table `tb_user_role` */
DROP TABLE IF EXISTS `tb_user_role`;
CREATE TABLE `tb_user_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '用户 ID',
`role_id` bigint(20) NOT NULL COMMENT '角色 ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户角色表';
/*Data for the table `tb_user_role` */
insert into `tb_user_role`(`id`,`user_id`,`role_id`) values
(1,1,1);
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
application.yml
spring:
application:
name: oauth2-server
datasource:
type: com.zaxxer.hikari.HikariDataSource
username: root
password: yali
url: jdbc:mysql://localhost:3306/oauth2?characterEncoding=utf-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&useSSL=true&zeroDateTimeBehavior=convertToNull&autoReconnect = true
hikari:
maximum-pool-size: 20
max-lifetime: 30000
idle-timeout: 30000
data-source-properties:
prepStmtCacheSize: 250
prepStmtCacheSqlLimit: 2048
cachePrepStmts: true
useServerPrepStmts: true
server:
port: 8080
mybatis:
mapper-locations: classpath:mapper/*.xml
启动类扫描mapper
@SpringBootApplication
@MapperScan(basePackages = "com.wzh.spring.security.oauth2.sever.mapper")
public class OAuth2ServerApplication {
public static void main(String[] args) {
SpringApplication.run(OAuth2ServerApplication.class,args);
}
}
查询和授权
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
TbUserService tbUserService;
@Autowired
TbPermissionService tbPermissionService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
TbUser tbUser = tbUserService.getByUsername(s);
if (tbUser != null) {
// 获取用户授权
List<TbPermission> tbPermissions = tbPermissionService.selectByUserId(tbUser.getId());
List<SimpleGrantedAuthority> collect = tbPermissions.stream().map(TbPermission::getEnname).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
// 由框架完成认证工作
return User.withUsername(tbUser.getUsername()).password(tbUser.getPassword()).authorities(collect).build();
}
return null;
}
}
完整代码在最后。
基于redis存储token令牌
与基于JDBC的区别:
- 增加spring-boot-starter-data-redis依赖
- application.yml增加redis配置
- 授权服务器注入RedisConnectionFactory,JdbcTokenStore改为RedisTokenStore
spring:
application:
name: oauth2-server
datasource:
type: com.zaxxer.hikari.HikariDataSource
username: root
password: yali
url: jdbc:mysql://localhost:3306/oauth2?characterEncoding=utf-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&useSSL=true&zeroDateTimeBehavior=convertToNull&autoReconnect = true
hikari:
maximum-pool-size: 20
max-lifetime: 30000
idle-timeout: 30000
data-source-properties:
prepStmtCacheSize: 250
prepStmtCacheSqlLimit: 2048
cachePrepStmts: true
useServerPrepStmts: true
redis:
host: localhost
password: 123456
port: 6379
server:
port: 8080
mybatis:
mapper-locations: classpath:mapper/*.xml
@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
BCryptPasswordEncoder passwordEncoder;
@Autowired
AuthenticationManager authenticationManager;
@Autowired
HikariDataSource hikariDataSource;
@Autowired
RedisConnectionFactory redisConnectionFactory;
//令牌生成方案 默认在内存中生成普通令牌
@Bean
public TokenStore tokenStore(){
return new RedisTokenStore(redisConnectionFactory);
}
public ClientDetailsService jdbcClientDetailsService(){
return new JdbcClientDetailsService(hikariDataSource);
}
// 配置客户端详情信息服务 客户端通过client_id和client_secret来访问资源
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(jdbcClientDetailsService());
}
// 令牌访问服务
@Bean
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setClientDetailsService(jdbcClientDetailsService());//客户端信息服务
tokenServices.setTokenStore(tokenStore());// 令牌生成方案
tokenServices.setRefreshTokenValiditySeconds(7200);//令牌默认有效期2小时
tokenServices.setAccessTokenValiditySeconds(259200);// 刷新令牌默认有效期3天
tokenServices.setSupportRefreshToken(true);// 设置刷新令牌
return tokenServices;
}
// 授权码模式
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
return new JdbcAuthorizationCodeServices(hikariDataSource);
}
// 令牌访问端点
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)//密码模式需要
.authorizationCodeServices(authorizationCodeServices())//授权码模式需要
.tokenStore(tokenStore())//令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST);//允许POST提交
}
// 令牌访问策略
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()") //oauth/token_key公开
.checkTokenAccess("permitAll()") //oauth/check_token公开
.allowFormAuthenticationForClients();//允许表单认证,申请令牌
}
}
资源服务器模块
流程图
- 增加一个服务spring-security-oauth2-resource
- 数据库增加内容管理权限模块,执行下面sql脚本
insert into `tb_permission`(`id`,`parent_id`,`name`,`enname`,`url`,`description`,`created`,`updated`) values
(7,1,'内容管理','SystemContent','/contents/',NULL,'2019-12-24 12:45:33','2019-12-24 12:45:33'),
(8,7,'查看内容','SystemContentView','/contents/view/**',NULL,'2019-12-24 12:45:33','2019-12-24 12:45:33'),
(9,7,'新增内容','SystemContentInsert','/contents/insert/**',NULL,'2019-12-24 12:45:33','2019-12-24 12:45:33'),
(10,7,'编辑内容','SystemContentUpdate','/contents/update/**',NULL,'2019-12-24 12:45:33','2019-12-24 12:45:33'),
(11,7,'删除内容','SystemContentDelete','/contents/delete/**',NULL,'2019-12-24 12:45:33','2019-12-24 12:45:33');
insert into `tb_role_permission`(`id`,`role_id`,`permission_id`) values
(7,1,7),
(8,1,8),
(9,1,9),
(10,1,10),
(11,1,11);
- 增加内容管理表和数据
CREATE TABLE `tb_content` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`category_id` bigint(20) NOT NULL COMMENT '内容类目ID',
`title` varchar(200) DEFAULT NULL COMMENT '内容标题',
`sub_title` varchar(100) DEFAULT NULL COMMENT '子标题',
`title_desc` varchar(500) DEFAULT NULL COMMENT '标题描述',
`url` varchar(500) DEFAULT NULL COMMENT '链接',
`pic` varchar(300) DEFAULT NULL COMMENT '图片绝对路径',
`pic2` varchar(300) DEFAULT NULL COMMENT '图片2',
`content` text COMMENT '内容',
`created` datetime DEFAULT NULL,
`updated` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `category_id` (`category_id`),
KEY `updated` (`updated`)
) ENGINE=InnoDB AUTO_INCREMENT=42 DEFAULT CHARSET=utf8;
insert into `tb_content`(`id`,`category_id`,`title`,`sub_title`,`title_desc`,`url`,`pic`,`pic2`,`content`,`created`,`updated`) values
(28,89,'标题','子标题','标题说明','http://www.jd.com',NULL,NULL,NULL,'2019-04-07 00:56:09','2019-04-07 00:56:11'),
(29,89,'ad2','ad2','ad2','http://www.baidu.com',NULL,NULL,NULL,'2019-04-07 00:56:13','2019-04-07 00:56:15'),
(30,89,'ad3','ad3','ad3','http://www.sina.com.cn',NULL,NULL,NULL,'2019-04-07 00:56:17','2019-04-07 00:56:19'),
(31,89,'ad4','ad4','ad4','http://www.funtl.com',NULL,NULL,NULL,'2019-04-07 00:56:22','2019-04-07 00:56:25');
CREATE TABLE `tb_content_category` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '类目ID',
`parent_id` bigint(20) DEFAULT NULL COMMENT '父类目ID=0时,代表的是一级的类目',
`name` varchar(50) DEFAULT NULL COMMENT '分类名称',
`status` int(1) DEFAULT '1' COMMENT '状态。可选值:1(正常),2(删除)',
`sort_order` int(4) DEFAULT NULL COMMENT '排列序号,表示同级类目的展现次序,如数值相等则按名称次序排列。取值范围:大于零的整数',
`is_parent` tinyint(1) DEFAULT '1' COMMENT '该类目是否为父类目,1为true,0为false',
`created` datetime DEFAULT NULL COMMENT '创建时间',
`updated` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `parent_id` (`parent_id`,`status`) USING BTREE,
KEY `sort_order` (`sort_order`)
) ENGINE=InnoDB AUTO_INCREMENT=98 DEFAULT CHARSET=utf8 COMMENT='内容分类';
insert into `tb_content_category`(`id`,`parent_id`,`name`,`status`,`sort_order`,`is_parent`,`created`,`updated`) values
(30,0,'LeeShop',1,1,1,'2015-04-03 16:51:38','2015-04-03 16:51:40'),
(86,30,'首页',1,1,1,'2015-06-07 15:36:07','2015-06-07 15:36:07'),
(87,30,'列表页面',1,1,1,'2015-06-07 15:36:16','2015-06-07 15:36:16'),
(88,30,'详细页面',1,1,1,'2015-06-07 15:36:27','2015-06-07 15:36:27'),
(89,86,'大广告',1,1,0,'2015-06-07 15:36:38','2015-06-07 15:36:38'),
(90,86,'小广告',1,1,0,'2015-06-07 15:36:45','2015-06-07 15:36:45'),
(91,86,'商城快报',1,1,0,'2015-06-07 15:36:55','2015-06-07 15:36:55'),
(92,87,'边栏广告',1,1,0,'2015-06-07 15:37:07','2015-06-07 15:37:07'),
(93,87,'页头广告',1,1,0,'2015-06-07 15:37:17','2015-06-07 15:37:17'),
(94,87,'页脚广告',1,1,0,'2015-06-07 15:37:31','2015-06-07 15:37:31'),
(95,88,'边栏广告',1,1,0,'2015-06-07 15:37:56','2015-06-07 15:37:56'),
(96,86,'中广告',1,1,1,'2015-07-25 18:58:52','2015-07-25 18:58:52'),
(97,96,'中广告1',1,1,0,'2015-07-25 18:59:43','2015-07-25 18:59:43');
- 增删改查业务正常写
- 配置资源服务器ResourceServerConfig继承ResourceServerConfigurerAdapter
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/insert").hasAuthority("SystemContentInsert")
.antMatchers("/update").hasAuthority("SystemContentUpdate");
}
}
application.yml
spring:
application:
name: oauth2-resource
datasource:
type: com.zaxxer.hikari.HikariDataSource
username: root
password: yali
url: jdbc:mysql://localhost:3306/oauth2?characterEncoding=utf-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&useSSL=true&zeroDateTimeBehavior=convertToNull&autoReconnect = true
hikari:
maximum-pool-size: 20
max-lifetime: 30000
idle-timeout: 30000
data-source-properties:
prepStmtCacheSize: 250
prepStmtCacheSqlLimit: 2048
cachePrepStmts: true
useServerPrepStmts: true
server:
port: 8081
servlet:
context-path: contents
security:
oauth2:
client:
client-id: client
client-secret: secret
access-token-uri: http://localhost:8080/oauth/token
user-authorization-uri: http://localhost:8080/oauth/authorize
resource:
token-info-uri: http://localhost:8080/oauth/check_token
mybatis:
mapper-locations: classpath:mapper/*.xml
请求
@RestController
public class TbContentController {
@Autowired
TbContentService tbContentService;
@GetMapping("/view")
@PreAuthorize("hasAuthority('SystemContentView')")
public ResponseResult<List<TbContent>> list(){
return new ResponseResult<>(HttpStatus.OK.value(),HttpStatus.OK.toString(),tbContentService.selectAll());
}
}