源代码地址:https://download.csdn.net/download/wllovar/10963011
具体那里有疑问可以给我发邮件[email protected]
最近应项目需求想搞一些类似微信公众平台那样的提供第三方访问的API,这就要用到OAuth2,具体OAuth2的认证流程如下图所示
好了废话不多说,我们开始干活
1.打开我们新建的SpringBoot项目,打开pom.xml,引入maven依赖包
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.1.RELEASE
com.pdl
springboot
0.0.1-SNAPSHOT
SpringBoot
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.postgresql
postgresql
runtime
mysql
mysql-connector-java
runtime
org.springframework.boot
spring-boot-starter-jdbc
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.2
org.springframework.boot
spring-boot-starter-aop
com.alibaba
druid-spring-boot-starter
1.1.9
com.github.pagehelper
pagehelper-spring-boot-starter
1.2.5
com.fasterxml.jackson.core
jackson-core
com.fasterxml.jackson.core
jackson-databind
com.fasterxml.jackson.datatype
jackson-datatype-joda
com.fasterxml.jackson.module
jackson-module-parameter-names
org.springframework.boot
spring-boot-starter-jta-atomikos
org.springframework.boot
spring-boot-starter-redis
1.4.7.RELEASE
org.springframework.boot
spring-boot-starter-security
org.springframework.security.oauth.boot
spring-security-oauth2-autoconfigure
2.0.3.RELEASE
org.springframework.boot
spring-boot-starter-data-redis
commons-lang
commons-lang
2.6
org.projectlombok
lombok
true
com.baomidou
mybatis-plus-boot-starter
3.0.4
io.springfox
springfox-swagger2
2.9.2
io.springfox
springfox-swagger-ui
2.9.2
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-maven-plugin
2.在config目录下新建认证服务端配置AuthorizationServerConfiguration.java
package com.pdl.config;
import com.pdl.security.BootClientDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private BootClientDetailsService clientDetailsService;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 允许表单登录
security.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
// tonken 存储于内存中
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)// add get method
.tokenStore(new InMemoryTokenStore())
.authenticationManager(authenticationManager);
}
}
3.在config目录下新建认证资源配置ResourceServerConfiguration.java
package com.pdl.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/register").permitAll()
.antMatchers("/druid/*").permitAll()
.anyRequest()
.authenticated();
}
}
注:如果需要开放某个接口不许认证操作时应该在ResourceServerConfiguration放开该方法的认证,具体实现是
.antMatchers("/register").permitAll()
如果不加这句话访问/register会一直报
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
这个坑坑我了一天,解决后特此标注一下,以便小伙伴们不再趟坑
4.在config目录下新建WebSecurity 配置
package com.pdl.config;
import com.pdl.security.BootUserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* Created by wangle on 2018/12/28.
*/
@Configuration
@EnableWebSecurity
@Order(1)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private BootUserDetailService userDetailService;
/**
* 让Security 忽略这些url,不做拦截处理
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers
("/swagger-ui.html/**", "/webjars/**",
"/swagger-resources/**", "/v2/api-docs/**",
"/swagger-resources/configuration/ui/**", "/swagger-resources/configuration/security/**",
"/images/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin().and()
.requestMatchers()
.antMatchers("/login","/oauth/authorize")
.and()
.authorizeRequests()
.anyRequest()
.authenticated();
http.httpBasic().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailService);
}
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
5.自定义自定义 UserDetailsService 和 BootClientDetailsService
package com.pdl.security;
import com.pdl.domain.User;
import com.pdl.service.AdminService;
import com.pdl.service.UserService;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class BootUserDetailService implements UserDetailsService {
@Autowired
private UserService userService;
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user= this.userService.selectUserByUsername(username);
if(user==null) {
throw new UsernameNotFoundException("用户名不存在");
}
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_USER");
List authorities = new ArrayList<>();
authorities.add(authority);
user.setAuthorities(authorities);
return user;
}
}
新建类 BootClientDetailsService
实现ClientDetailsService
接口,覆盖loadClientByClientId(String clientId)
方法,将其声明为spring组件,方便后面配置使用
package com.pdl.security;
import com.pdl.domain.Client;
import com.pdl.service.AdminService;
import com.pdl.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.stereotype.Component;
@Component
public final class BootClientDetailsService implements ClientDetailsService {
@Autowired
private UserService userService;
@Override
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
Client client = userService.selectClientByClientId(clientId);
if(client==null){
throw new ClientRegistrationException("客户端不存在");
}
return new BootClientDetails(client);
}
}
package com.pdl.security;
import com.pdl.domain.Client;
import com.pdl.util.CommonUtils;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.provider.ClientDetails;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
/**
* @author wangle
* @date 2018/10/16 15:36
*
**/
@Data
@SuppressWarnings("unchecked")
public final class BootClientDetails implements ClientDetails {
private Client client;
private Set scope;
public BootClientDetails(Client client) {
this.client = client;
}
public BootClientDetails() {
}
@Override
public String getClientId() {
return client.getClientId();
}
@Override
public Set getResourceIds() {
return client.getResourceIds()!=null?
CommonUtils.transformStringToSet(client.getResourceIds(),String.class):null;
}
@Override
public boolean isSecretRequired() {
return client.getIsSecretRequired();
}
@Override
public String getClientSecret() {
return client.getClientSecret();
}
@Override
public boolean isScoped() {
return client.getIsScoped();
}
@Override
public Set getScope() {
this.scope = client.getScope()!=null?
CommonUtils.transformStringToSet(client.getScope(),String.class):null;
return client.getScope()!=null?
CommonUtils.transformStringToSet(client.getScope(),String.class):null;
}
@Override
public Set getAuthorizedGrantTypes() {
return client.getAuthorizedGrantTypes()!=null?
CommonUtils.transformStringToSet(client.getAuthorizedGrantTypes(),String.class):null;
}
@Override
public Set getRegisteredRedirectUri() {
return client.getRegisteredRedirectUri()!=null?
CommonUtils.transformStringToSet(client.getRegisteredRedirectUri(),String.class):null;
}
@Override
public Collection getAuthorities() {
return (client.getAuthorities()!=null&&client.getAuthorities().trim().length()>0)?
AuthorityUtils.commaSeparatedStringToAuthorityList(client.getAuthorities()):null;
}
@Override
public Integer getAccessTokenValiditySeconds() {
return client.getAccessTokenValiditySeconds();
}
@Override
public Integer getRefreshTokenValiditySeconds() {
return client.getRefreshTokenValiditySeconds();
}
@Override
public boolean isAutoApprove(String scope) {
return this.client.getIsAutoApprove()==null ? false: this
.client.getIsAutoApprove();
}
@Override
public Map getAdditionalInformation() {
return null;
}
}
6.在DAO层新建UserMapper.java
package com.pdl.dao.crm;
import com.pdl.domain.Client;
import com.pdl.domain.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.HashMap;
import java.util.List;
@Mapper
public interface UserMapper {
List selectAllUser();
void insert(HashMap params);
User selectUserByUserName( @Param("username") String username);
Client selectClientByClientId(@Param("clientId") String clientId);
int insertClient(Client client);
}
7.在domain层新建实体类Client.java和User.java
package com.pdl.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* @author wangle
* @date 2018/10/16 9:23
*
**/
@Data
@TableName("clients")
public class Client {
@TableId
private String id;
@TableField("clientId")
@NotNull
private String clientId;
@TableField("resourceIds")
private String resourceIds;
@TableField("isSecretRequired")
private Boolean isSecretRequired;
@TableField("clientSecret")
@NotNull
private String clientSecret;
@TableField("isScoped")
private Boolean isScoped;
@TableField("scope")
private String scope;
@TableField("authorizedGrantTypes")
@NotNull
private String authorizedGrantTypes;
@TableField("registeredRedirectUri")
@NotNull
private String registeredRedirectUri;
@TableField("authorities")
private String authorities;
@TableField("isAutoApprove")
private Boolean isAutoApprove;
@TableField("accessTokenValiditySeconds")
@NotNull
private Integer accessTokenValiditySeconds;
@TableField("refreshTokenValiditySeconds")
@NotNull
private Integer refreshTokenValiditySeconds;
@TableField("createTime")
@NotNull
private String createTime;
@TableField("modifyTime")
@NotNull
private String modifyTime;
}
package com.pdl.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
/**
* @author wangle
* @date 2018/10/9 15:43
*
**/
@Data
@TableName("user")
public class User implements UserDetails {
@TableId
private String id;
private String username;
private String email;
@TableField("isEnable")
private Boolean isEnable;
@TableField("isExpired")
private Boolean isExpired;
@TableField("isLocked")
private Boolean isLocked;
private String password;
private String gender;
@TableField(exist = false)
private List authorities;
@Override
public List extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return this.isLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.isEnable;
}
}
8.在service层新建UserService类
package com.pdl.service;
import com.pdl.dao.crm.UserMapper;
import com.pdl.domain.Client;
import com.pdl.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public Map selectUser(){
Map data = new HashMap();
List userList = userMapper.selectAllUser();
data.put("recordList",userList);
data.put("total",userList.size());
return data;
}
public User selectUserByUsername(String username){
User user = userMapper.selectUserByUserName(username);
return user;
}
public Client selectClientByClientId(String clientId){
Client client = userMapper.selectClientByClientId(clientId);
return client;
}
public boolean addClient(Client client){
boolean flag = false;
int res = userMapper.insertClient(client);
if(res==1){
flag = true;
}
return flag;
}
}
9.在controller层新建ClientController.java
package com.pdl.controller;
import com.pdl.domain.Client;
import com.pdl.security.response.BaseResponse;
import com.pdl.security.response.HttpResponse;
import com.pdl.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.UUID;
@RestController
public class ClientController {
@Autowired
private UserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/register")
public BaseResponse clientRegistered(@RequestBody @Valid Client client){
client.setId(UUID.randomUUID().toString());
client.setClientSecret(passwordEncoder.encode(client.getClientSecret()));
boolean i = userService.addClient(client);
return HttpResponse.baseResponse(200);
}
}
10.配置application.yml文件
server:
port: 8060
spring:
datasource:
druid:
crm:
#配置监控统计拦截的filters,去掉后监控界面SQL无法进行统计,'wall'用于防火墙
filters: stat,wall
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/pdl_oauth?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=GMT%2B8
username: root
password: 123456
#初始化大小
initial-size: 50
#最小连接数
min-idle: 50
#最大连接数
max-active: 200
#获取连接等待超时时间
max-wait: 60000
#间隔多久才进行一次检测,检测需要关闭的空闲连接,单位毫秒
time-between-eviction-runs-millis: 60000
#一个连接在池中最小生存的时间,单位是毫秒
min-evictable-idle-time-millis: 30000
#测试语句是否执行正确
validation-query: SELECT 'x'
#指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除.
test-while-idle: true
#借出连接时不要测试,否则很影响性能
test-on-borrow: false
test-on-return: false
#打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
pool-prepared-statements: false
#与Oracle数据库PSCache有关,再druid下可以设置的比较高
max-pool-prepared-statement-per-connection-size: 20
#数据源2
hr:
filters: stat,wall
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/pdl_hr?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
username: postgres
password: 123456
initial-size: 50
min-idle: 50
max-active: 200
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 30000
validation-query: SELECT 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
redis:
host: 127.0.0.1
port: 6379
jedis:
pool:
max-active: 8
max-wait: -1s
min-idle: 0
max-idle: 8
security:
到此为止我们的项目搭建工作已经完成
访问http://localhost:8060/oauth/authorize?response_type=code&client_id=lovar&redirect_uri=http://www.baidu.com&scope=all
输入账号:admin ,密码:123qwe,进行登录进入授权页面
授权完成后会获得授权码https://www.baidu.com/?code=OJN1hB
接下来在postman上进行操作
授权码的操作已完成,下面我们开始测试注册客户端,打开http://localhost:8060/swagger-ui.html
访问/register接口进行客户端注册
{
"accessTokenValiditySeconds": 1800,
"authorities": "ADMIN",
"authorizedGrantTypes": "refresh_token,authorization_code",
"clientId": "lovar",
"clientSecret": "123456",
"isAutoApprove": false,
"isSecretRequired": true,
"refreshTokenValiditySeconds": 3600,
"registeredRedirectUri": "http://localhost:8060",
"scope": "all",
"scoped": true,
"secretRequired": true,
"createTime": "2019-02-18 00:12:10",
"modifyTime": "2019-02-18 00:12:10",
"resourceIds": "boot-server"
}