第十八章:SpringBoot项目中使用SpringSecurity整合OAuth2设计项目API安全接口服务

新建项目,引入依赖(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

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SL_Home

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值