新建项目,引入依赖(web,security,jpa,mysql,druid,oauth2,thymeleaf)
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.29</version>
</dependency>
<!--security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--oauth2依赖-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
建表语句
安全用户信息表(用户信息表包含了简单的登录名、密码、邮箱、状态等)
create table userinfo(username varchar(50) primary key,email varchar(50),password varchar(500),activated int,activationkey varchar(50),resetpasswordkey varchar(50));
安全角色信息表
create table role(name varchar(50) primary key);
用户与角色关联表
create table user_role(username varchar(50),authority varchar(50));
access_token信息表
我们使用的是SpringSecurityOAuth2提供的Jdbc方式进行操作Token,所以需要根据标准创建对应的表结构,oauth_access_token信息表结构如下
CREATE TABLE oauth_access_token (
`token_id` VARCHAR(256) NULL DEFAULT NULL,
`token` BLOB NULL DEFAULT NULL,
`authentication_id` VARCHAR(128) NOT NULL,
`user_name` VARCHAR(256) NULL DEFAULT NULL,
`client_id` VARCHAR(256) NULL DEFAULT NULL,
`authentication` BLOB NULL DEFAULT NULL,
`refresh_token` VARCHAR(256) NULL DEFAULT NULL,
PRIMARY KEY (`authentication_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
RefreshToken信息表
刷新Token时需要用到oauth_refresh_token信息表结构如下
CREATE TABLE oauth_refresh_token (
`token_id` VARCHAR(256) NULL DEFAULT NULL,
`token` BLOB NULL DEFAULT NULL,
`authentication` BLOB NULL DEFAULT NULL)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
创建实体:
我们只需要创建用户信息、角色信息的实体即可,因为OAuth2内部操作数据库使用的JdbcTemplate我们只需要传入一个DataSource对象就可以了,实体并不需要配置。
RoleEntity.java
RoleEntity.java
@Entity
@Table(name = "role")
public class RoleEntity {
@Id
@Column(name = "name")
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RoleEntity that = (RoleEntity) o;
return Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
@Override
public String toString() {
return "RoleEntity{" +
", name='" + name + '\'' +
'}';
}
}
UserInfoEntity.java
@Entity
@Table(name = "userinfo")
public class UserInfoEntity {
@Id
@GeneratedValue
@Column(name = "username")
private String username;
@Column(name = "email")
private String email;
@Column(name = "password")
private String password;
@Column(name = "activated")
private Integer activated;
@Column(name = "activationkey")
private String activationkey;
@Column(name = "resetpasswordkey")
private String resetpasswordkey;
//权限
@ManyToMany
@JoinTable(name = "user_role",joinColumns = @JoinColumn(name = "username")
,inverseJoinColumns = @JoinColumn(name = "authority"))
private Set<RoleEntity> authorities;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserInfoEntity that = (UserInfoEntity) o;
return Objects.equals(username, that.username) &&
Objects.equals(email, that.email) &&
Objects.equals(password, that.password) &&
Objects.equals(activated, that.activated) &&
Objects.equals(activationkey, that.activationkey) &&
Objects.equals(resetpasswordkey, that.resetpasswordkey);
}
@Override
public int hashCode() {
return Objects.hash(username, email, password, activated, activationkey, resetpasswordkey);
}
@Override
public String toString() {
return "UserInfoEntity{" +
"username='" + username + '\'' +
", email='" + email + '\'' +
", password='" + password + '\'' +
", activated=" + activated +
", activationkey='" + activationkey + '\'' +
", resetpasswordkey='" + resetpasswordkey + '\'' +
'}';
}
public Set<RoleEntity> getAuthorities() {
return authorities;
}
public void setAuthorities(Set<RoleEntity> authorities) {
this.authorities = authorities;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getActivated() {
return activated;
}
public void setActivated(Integer activated) {
this.activated = activated;
}
public String getActivationkey() {
return activationkey;
}
public void setActivationkey(String activationkey) {
this.activationkey = activationkey;
}
public String getResetpasswordkey() {
return resetpasswordkey;
}
public void setResetpasswordkey(String resetpasswordkey) {
this.resetpasswordkey = resetpasswordkey;
}
}
配置UserJpa、RoleJpa
RoleJpa.java
@Repository
public interface Role extends JpaRepository<RoleEntity,Integer> {
}
UserInfoJpa.java
public interface UserInfoJpa extends JpaRepository<UserInfoEntity,Integer> {
UserInfoEntity findByUsername(String username);
}
配置yml文件:
server:
port: 9021
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
username: root
password: Sunlu1994
url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8
type: com.alibaba.druid.pool.DruidDataSource
max-active: 50 #最大活跃数
initial-size: 1 #初始化数量
max-wait: 60000 #最大连接等待时间
filters: stat #配置监控统计拦截的filters,去掉后监控界面的sql无法统计,wall用于防火墙
minIdle: 1
poolPreparedStatements: true #打开PSCache
maxOpenPreparedStatements: 20 #指定每个连接的PSCache大小
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: select 1 from dual
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 #打开mergeSql功能
jpa:
hibernate:
ddl-auto: update
show-sql: true
#在UserEntity中,有多个属性:name,password,phone等,还有一个Set类型的 Set<RoleEntity> authorities。
#当我们对authorities使用懒加载(lazy laoding)的时候,hibernate会在获得UserEntity对象的时候,
#仅仅返回 name,password,phone 等基本属性,当你访问authories的时候,
#它才会从数据库中提取 articleList 需要的数据,这就是所谓lazy laoding。但是在我们的系统中,session是被立即关闭的,
#也就是在读取了name,password,phone等基本属性后,session 已经 close了,再进行 lazy loaiding 就会有异常。
#所以要配置如下内容
properties:
hibernate:
enable_lazy_load_no_trans: true
thymeleaf:
prefix: classpath:/templates/
suffix: .html
#自定义配置在Oauth2Configuration中认证服务器中使用
authentication:
oauth:
clientid: sunlu
secret: 12
tokenValiditySeconds: 1800
#这个配置的意思时,将我们的资源拦截的过滤器运行顺序放到第3个执行,也就是在oauth2的认证服务器后面执行
security:
oauth2:
resource:
filter-order: 3
创建控制器
SecureController.java
@RestController
@RequestMapping("/secure")
public class SecureController {
@RequestMapping(method = RequestMethod.GET)
public String sayHello(){
return "secure hello!";
}
}
HelloWorldController.java
@RestController
@RequestMapping(value = "/hello")
public class HelloWorldController {
@RequestMapping(method = RequestMethod.GET)
public String sayHello(){
return "hello User!";
}
}
自定义UserServices继承自UserDetailService ,配置读取用户名密码信息。
@Component
public class UserService implements UserDetailsService {
@Autowired
private UserInfoJpa userInfoJpa;
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserInfoEntity userInfoEntity = userInfoJpa.findByUsername(username);
if (userInfoEntity==null){
throw new UsernameNotFoundException("用户名不存在");
}
// TODO 根据用户名,查找到对应的密码,与权限
System.out.println(userInfoEntity.getAuthorities());
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for (RoleEntity roleEntity:userInfoEntity.getAuthorities()) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(roleEntity.getName());
grantedAuthorities.add(grantedAuthority);
}
// 封装用户信息,并返回。参数分别是:用户名,密码,用户权限
User user = new User(username, userInfoEntity.getPassword(),
grantedAuthorities);
return user;
}
/*
因为Spring-Security从4+升级到5+,导致There is no PasswordEncoder mapped for the id “null”错误。
解决方法:可在密码验证类中添加如下方法
*/
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
}
下面我们来配置SpringSecurity相关的内容,新建SecurityConfiguration
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
//配置自定义注入UserService
private UserService userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 设置userDetailsService
auth.userDetailsService(userService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//定义安全配置,定义/hello不需要安全验证
http.authorizeRequests()
.antMatchers("/hello").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.csrf().disable(); // 关闭csrf防护
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
我们在配置类中注入了上面我们自定义的HengYuUserDetailsService以及用户密码验证规则,排除了对/hello的拦截
自定义401错误码内容
我们上图已经用到了对应的类CustomAuthenticationEntryPoint,该类是用来配置如果没有权限访问接口时我们返回的错误码以及错误内容
CustomAuthenticationEntryPoint.java
/*
配置如果没有权限,访问接口时我们返回的错误码以及错误内容
*/
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final Logger logger = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
logger.info("");
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,"access denied");
}
}
定义登出控制
当我们退出系统时需要访问SpringSecrutiy的logout方法来清空对应的session信息,那我们退出后改用户的access_token还依然存在那就危险了,一旦别人知道该token就可以使用之前登录用户的权限来操作业务
CustomLogoutSuccessHandler.java
/*
当我们退出系统时需要访问SpringSecrutiy的logout方法来清空对应的session信息,
那我们退出后改用户的access_token还依然存在那就危险了,一旦别人知道该token就可以使用之前登录用户的权限来操作业务。
*/
@Component
public class CustomLogoutSuccessHandler extends AbstractAuthenticationTargetUrlRequestHandler implements LogoutSuccessHandler {
private static final String BEARER_AUTHENTICATION="Bearer";
private static final String HEADER_AUTHORIZATION="authorization";
@Autowired
private TokenStore tokenStore;
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
String token = httpServletRequest.getHeader(HEADER_AUTHORIZATION);
if (token!=null&&token.startsWith(BEARER_AUTHENTICATION)){
OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(token.split(" ")[0]);
if (oAuth2AccessToken!=null){
tokenStore.removeAccessToken(oAuth2AccessToken);
}
}
}
}
配置相关OAuth2的内容,我们创建一个OAuth2总配置类OAuth2Configuration,类内添加一个子类用于配置资源服务器,一个子类用于开启OAuth2的验证服务器
Oauth2Configuration.java
@Configuration
public class Oauth2Configuration {
//资源服务器
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter{
@Autowired
CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
@Autowired
CustomLogoutSuccessHandler customLogoutSuccessHandler;
@Override
public void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint)
.and()
.logout()
.logoutSuccessHandler(customLogoutSuccessHandler)
.and()
.authorizeRequests()
.antMatchers("/hello","/login").permitAll()
.antMatchers("/secure/**").authenticated()
.anyRequest().authenticated();
}
}
@Configuration
@EnableAuthorizationServer
//认证服务器
public static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter{
//从yml读取数据
@Value("${authentication.oauth.clientid}")
private String clientid;
@Value("${authentication.oauth.secret}")
private String secret;
@Value("${authentication.oauth.tokenValiditySeconds}")
private String tokenValiditySeconds;
@Autowired
private DataSource dataSource;
@Autowired
private UserService userService;
@Bean
public TokenStore tokenStore(){
return new JdbcTokenStore(dataSource);
}
@Bean
@Primary
public DefaultTokenServices tokenServices() {
//TestDefaultTokenServices设置createToken线程锁(创建token和刷新token)
DefaultTokenServices defaultTokenServices = new TestDefaultTokenServices();
defaultTokenServices.setSupportRefreshToken(true);
defaultTokenServices.setReuseRefreshToken(false);
defaultTokenServices.setTokenStore(tokenStore());
// tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
return defaultTokenServices;
}
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userService);
//解决token并发问题
endpoints.tokenServices(tokenServices());
}
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
// 开启/oauth/token_key验证端口无权限访问
.tokenKeyAccess("permitAll()")
// 开启/oauth/check_token验证端口认证权限访问
//配置了 /oauth/check_token
//不写check_token会报错
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
logger.info("result:"+clientid);
clients.inMemory()
.withClient(clientid)
.scopes("all")
.authorities(Authorities.ROLE_ADMIN.name(),Authorities.ROLE_USER.name())
.authorizedGrantTypes("password","refresh_token")
.secret(secret)
.accessTokenValiditySeconds(Integer.parseInt(tokenValiditySeconds));
}
}
}
TestDefaultTokenServices.java 重写父类方法,添加线程锁,同步执行创建token、刷新token,解决token高并发问题
TestDefaultTokenServices.java
public class TestDefaultTokenServices extends DefaultTokenServices {
@Override
public synchronized OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
return super.createAccessToken(authentication);
}
@Override
public synchronized OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest) throws AuthenticationException {
return super.refreshAccessToken(refreshTokenValue, tokenRequest);
}
}
Authorities.java
public enum Authorities {
ROLE_ANONYMOUS,
ROLE_USER,
ROLE_ADMIN,
}
源码地址:链接: https://pan.baidu.com/s/1KWTxhMMIptIFers0jyTFxw 提取码: 42fe
请求地址:127.0.0.1:8080/hello(可以直接访问,排除了拦截)
获取token地址(password模式,使用psotman发起post请求):
localhost:9021/oauth/token?username=sun&password=12&grant_type=password
验证token地址(自己改token的值):http://localhost:9021/oauth/check_token?token=804366f7-ed47-4d8b-b07a-2c6a3acd0f5c
刷新token地址(自己改token的值):http://localhost:9021/oauth/token?grant_type=refresh_token&refresh_token=f8ce8b96-7596-4200-8cdb-dc287b1db81e&scope=all
访问资源地址(自己改token的值):localhost:9021/security?access_token=fdabe501-d4c4-43ef-9648-7be0edd2cd2b