SpringSecurity实现OAuth2.0 - 进阶版授权服务

《SpringSecurity实现OAuth2.0_01_基础版授权服务》介绍了如何使用Spring Security实现OAuth2.0授权和资源保护,但是使用的都是Spring Security默认的登录页、授权页,client和token信息也是保存在内存中的。

本文将介绍如何在Spring Security OAuth项目中自定义登录页面、自定义授权页面、数据库配置client信息、数据库保存授权码和token令牌。

引入依赖

需要在基础版之上引入thymeleaf、JDBC、mybatis、mysql等依赖。

<!-- thymeleaf -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>

<!-- JDBC -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-dbcp2</artifactId>
</dependency>

<!-- Mybatis -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.1.1</version>
</dependency>

<!-- MySQL -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

数据库表

client和token表

-- used in tests that use HSQL
create table oauth_client_details (
  client_id VARCHAR(255) PRIMARY KEY,
  resource_ids VARCHAR(255),
  client_secret VARCHAR(255),
  scope VARCHAR(255),
  authorized_grant_types VARCHAR(255),
  web_server_redirect_uri VARCHAR(255),
  authorities VARCHAR(255),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additional_information TEXT(4096),
  autoapprove VARCHAR(255)
);

create table oauth_client_token (
  token_id VARCHAR(255),
  token BLOB,
  authentication_id VARCHAR(255) PRIMARY KEY,
  user_name VARCHAR(255),
  client_id VARCHAR(255)
);

create table oauth_access_token (
  token_id VARCHAR(255),
  token BLOB,
  authentication_id VARCHAR(255) PRIMARY KEY,
  user_name VARCHAR(255),
  client_id VARCHAR(255),
  authentication BLOB,
  refresh_token VARCHAR(255)
);

create table oauth_refresh_token (
  token_id VARCHAR(255),
  token BLOB,
  authentication BLOB
);

create table oauth_code (
  code VARCHAR(255), authentication BLOB
);

create table oauth_approvals (
    userId VARCHAR(255),
    clientId VARCHAR(255),
    scope VARCHAR(255),
    status VARCHAR(10),
    expiresAt TIMESTAMP,
    lastModifiedAt TIMESTAMP
);


-- customized oauth_client_details table
create table ClientDetails (
  appId VARCHAR(255) PRIMARY KEY,
  resourceIds VARCHAR(255),
  appSecret VARCHAR(255),
  scope VARCHAR(255),
  grantTypes VARCHAR(255),
  redirectUrl VARCHAR(255),
  authorities VARCHAR(255),
  access_token_validity INTEGER,
  refresh_token_validity INTEGER,
  additionalInformation VARCHAR(4096),
  autoApproveScopes VARCHAR(255)
);

在oauth_client_details表添加数据:

INSERT INTO `oauth_client_details` VALUES ('net5ijy', NULL, '123456', 'all,read,write', 'authorization_code,refresh_token,password', NULL, 'ROLE_TRUSTED_CLIENT', 7200, 7200, NULL, NULL);

INSERT INTO `oauth_client_details` VALUES ('tencent', NULL, '123456', 'all,read,write', 'authorization_code,refresh_code', NULL, 'ROLE_TRUSTED_CLIENT', 3600, 3600, NULL, NULL);

用户、角色表

CREATE TABLE `springcloud_user` (
`id`  int(11) NOT NULL AUTO_INCREMENT ,
`username`  varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
`password`  varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
`phone`  varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
`email`  varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
`create_time`  datetime NOT NULL ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci
AUTO_INCREMENT=1;

CREATE TABLE `springcloud_role` (
`id`  int(11) NOT NULL AUTO_INCREMENT ,
`name`  varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci
AUTO_INCREMENT=1;

CREATE TABLE `springcloud_user_role` (
`user_id`  int(11) NOT NULL ,
`role_id`  int(11) NOT NULL ,
FOREIGN KEY (`role_id`) REFERENCES `springcloud_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
FOREIGN KEY (`user_id`) REFERENCES `springcloud_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
INDEX `user_id_fk` USING BTREE (`user_id`) ,
INDEX `role_id_fk` USING BTREE (`role_id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci;

角色表添加数据:

INSERT INTO `springcloud_role` VALUES (1, 'ADMIN');
INSERT INTO `springcloud_role` VALUES (2, 'DBA');
INSERT INTO `springcloud_role` VALUES (3, 'USER');

用户角色关系表添加数据:

INSERT INTO `springcloud_user_role` VALUES (1, 1);
INSERT INTO `springcloud_user_role` VALUES (2, 1);

实体类和工具类

User实体类

封装授权服务器登录用户信息:

public class User implements Serializable {

    private Integer id;
    private String username;
    private String password;
    private String phone;
    private String email;
    private Set<Role> roles = new HashSet<Role>();
    private Date createTime;

    // getter & setter

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((id == null) ? 0 : id.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        User other = (User) obj;
        if (id == null) {
            if (other.id != null)
                return false;
        } else if (!id.equals(other.id))
            return false;
        return true;
    }
}

Role实体类

封装角色信息:

public class Role implements Serializable {

    private Integer id;
    private String name;

    public Role() {
        super();
    }
    public Role(String name) {
        super();
        this.name = name;
    }

    // getter & setter
}

ResponseMessage工具类

封装接口响应信息:

public class ResponseMessage {

    private Integer code;
    private String message;

    public ResponseMessage() {
        super();
    }

    public ResponseMessage(Integer code, String message) {
        super();
        this.code = code;
        this.message = message;
    }

    // getter & setter

    public static ResponseMessage success() {
        return new ResponseMessage(0, "操作成功");
    }

    public static ResponseMessage fail() {
        return new ResponseMessage(99, "操作失败");
    }
}

DAO和Service编写

数据源配置

在application.properties文件配置datasource:

spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=system
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.type=org.apache.commons.dbcp2.BasicDataSource
spring.datasource.dbcp2.initial-size=5
spring.datasource.dbcp2.max-active=25
spring.datasource.dbcp2.max-idle=10
spring.datasource.dbcp2.min-idle=5
spring.datasource.dbcp2.max-wait-millis=10000
spring.datasource.dbcp2.validation-query=SELECT 1
spring.datasource.dbcp2.connection-properties=characterEncoding=utf8

使用dbcp2数据源。

mapper.xml

在src/main/resources下创建org.net5ijy.oauth2.mapper包,创建user-mapper.xml配置文件:

<mapper namespace="org.net5ijy.oauth2.repository.UserRepository">

    <resultMap type="User" id="UserResultMap">
        <result column="id" property="id" jdbcType="INTEGER" javaType="int" />
        <result column="username" property="username" jdbcType="VARCHAR"
            javaType="string" />
        <result column="password" property="password" jdbcType="VARCHAR"
            javaType="string" />
        <result column="phone" property="phone" jdbcType="VARCHAR"
            javaType="string" />
        <result column="email" property="email" jdbcType="VARCHAR"
            javaType="string" />
        <result column="create_time" property="createTime" jdbcType="TIMESTAMP"
            javaType="java.util.Date" />
        <collection property="roles" select="selectRolesByUserId"
            column="id"></collection>
    </resultMap>

    <!-- 根据用户名查询用户 -->
    <select id="findByUsername" parameterType="java.lang.String"
        resultMap="UserResultMap">
        <![CDATA[
        select * from springcloud_user where username = #{username}
        ]]>
    </select>

    <!-- 根据user id查询用户拥有的role -->
    <select id="selectRolesByUserId" parameterType="java.lang.Integer"
        resultType="Role">
        <![CDATA[
        select r.id, r.name from springcloud_user_role ur, springcloud_role r
        where ur.role_id = r.id and ur.user_id = #{id}
        ]]>
    </select>

</mapper>

因为我们的例子只使用了findByUsername功能,所以只写这个sql就可以了。

DAO接口

public interface UserRepository {
    User findByUsername(String username);
}

UserService

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public User getUser(String username) {
        return userRepository.findByUsername(username);
    }
}

UserDetailsService实现类

这个接口的实现类需要在Security中配置,Security会使用这个类根据用户名查询用户信息,然后进行用户名、密码的验证。主要就是实现loadUserByUsername方法:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.getUser(username);
        if (user == null || user.getId() < 1) {
            throw new UsernameNotFoundException("Username not found: " + username);
        }
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(), user.getPassword(), true, true, true, true,
                getGrantedAuthorities(user));
    }

    private Collection<? extends GrantedAuthority> getGrantedAuthorities(User user) {
        Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
        for (Role role : user.getRoles()) {
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
        }
        return authorities;
    }
}

自定义登录页面

controller编写

编写LoginController类,添加login方法:

@RestController
public class LoginController {

    @GetMapping("/login")
    public ModelAndView login() {
        return new ModelAndView("login");
    }

    @GetMapping("/login-error")
    public ModelAndView loginError(HttpServletRequest request, Model model) {
        model.addAttribute("loginError", true);
        model.addAttribute("errorMsg", "登陆失败,账号或者密码错误!");
        return new ModelAndView("login", "userModel", model);
    }
}

页面代码

页面代码使用到了thymeleaf、bootstrap、表单验证等,具体的js、css引入就不赘述了,只记录最主要的内容:

<div>
    <form th:action="@{/login}" method="post">
        <div>
            <label>用 户 名: </label>
            <div>
                <input name="username" />
            </div>
        </div>
        <div>
            <label>密  码: </label>
            <div>
                <input type="password" name="password" />
            </div>
        </div>
        <div>
            <div>
                <button type="submit"> 登 陆 </button>
            </div>
        </div>
    </form>
</div>

自定义授权页面

controller编写

编写GrantController类,添加getAccessConfirmation方法:

@Controller
@SessionAttributes("authorizationRequest")
public class GrantController {

    @RequestMapping("/oauth/confirm_access")
    public ModelAndView getAccessConfirmation(
            Map<String, Object> model,
            HttpServletRequest request) throws Exception {

        AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get("authorizationRequest");

        ModelAndView view = new ModelAndView("base-grant");
        view.addObject("clientId", authorizationRequest.getClientId());

        return view;
    }
}

此处获取到申请授权的clientid用于在页面展示。

页面代码

此处只写最主要的部分:

<div>
    <div>
        <div>OAUTH-BOOT 授权</div>
        <div>
            <a href="javascript:;">帮助</a>
        </div>
    </div>
    <h3 th:text="${clientId}+' 请求授权,该应用将获取您的以下信息'"></h3>
    <p>昵称,头像和性别</p>
    授权后表明您已同意 <a href="javascript:;" style="color: #E9686B">OAUTH-BOOT 服务协议</a>
    <form method="post" action="/oauth/authorize">
        <input type="hidden" name="user_oauth_approval" value="true" />
        <input type="hidden" name="scope.all" value="true" />
        <br />
        <button class="btn" type="submit">同意/授权</button>
    </form>
</div>

配置类和application.properties配置

配置mybatis

配置SqlSessionFactoryBean:

  • 设置数据源
  • 设置包别名
  • 设置mapper映射文件所在的包
@Configuration
public class MyBatisConfiguration {

    @Bean
    @Autowired
    @ConditionalOnMissingBean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) throws IOException {

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

        // 设置数据源
        sqlSessionFactoryBean.setDataSource(dataSource);

        // 设置别名包
        sqlSessionFactoryBean.setTypeAliasesPackage("org.net5ijy.oauth2.bean");

        // 设置mapper映射文件所在的包
        PathMatchingResourcePatternResolver pathMatchingResourcePatternResolver =
            new PathMatchingResourcePatternResolver();
        String packageSearchPath = "classpath*:org/net5ijy/oauth2/mapper/**.xml";
        sqlSessionFactoryBean
                .setMapperLocations(pathMatchingResourcePatternResolver
                        .getResources(packageSearchPath));

        return sqlSessionFactoryBean;
    }
}

配置AuthorizationServerConfigurer

  • 配置使用数据库保存cient信息
  • 配置使用数据库保存token令牌
  • 配置使用数据库保存授权码
@Configuration
public class Oauth2AuthorizationServerConfiguration extends
        AuthorizationServerConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private DataSource dataSource;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 数据库管理client
        clients.withClientDetails(new JdbcClientDetailsService(dataSource));
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        // 用户信息查询服务
        endpoints.userDetailsService(userDetailsService);

        // 数据库管理access_token和refresh_token
        TokenStore tokenStore = new JdbcTokenStore(dataSource);

        endpoints.tokenStore(tokenStore);

        ClientDetailsService clientService = new JdbcClientDetailsService(dataSource);

        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(tokenStore);
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setClientDetailsService(clientService);
        // tokenServices.setAccessTokenValiditySeconds(180);
        // tokenServices.setRefreshTokenValiditySeconds(180);

        endpoints.tokenServices(tokenServices);

        endpoints.authenticationManager(authenticationManager);

        // 数据库管理授权码
        endpoints.authorizationCodeServices(new JdbcAuthorizationCodeServices(dataSource));
        // 数据库管理授权信息
        ApprovalStore approvalStore = new JdbcApprovalStore(dataSource);
        endpoints.approvalStore(approvalStore);
    }
}

配置security

  • 配置使用数据库保存登录用户信息
  • 配置自定义登录页面
  • 暂时禁用CSRF
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // 使用BCrypt加密
    }

    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder);
        authenticationProvider.setHideUserNotFoundExceptions(false);
        return authenticationProvider;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/js/**", "/fonts/**", "/icon/**", "/favicon.ico");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.requestMatchers()
                .antMatchers("/login", "/login-error", "/oauth/authorize",
                        "/oauth/token").and().authorizeRequests()
                .antMatchers("/login").permitAll().anyRequest().authenticated();

        // 登录页面
        http.formLogin().loginPage("/login").failureUrl("/login-error");

        // 禁用CSRF
        http.csrf().disable();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
        auth.authenticationProvider(authenticationProvider());
    }
}

application.properties文件配置

server.port=7001

##### Built-in DataSource #####
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=system
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.type=org.apache.commons.dbcp2.BasicDataSource
spring.datasource.dbcp2.initial-size=5
spring.datasource.dbcp2.max-active=25
spring.datasource.dbcp2.max-idle=10
spring.datasource.dbcp2.min-idle=5
spring.datasource.dbcp2.max-wait-millis=10000
spring.datasource.dbcp2.validation-query=SELECT 1
spring.datasource.dbcp2.connection-properties=characterEncoding=utf8

##### Thymeleaf #####
# 编码
spring.thymeleaf.encoding=UTF-8
# 热部署静态文件
spring.thymeleaf.cache=false
# 使用HTML5标准
spring.thymeleaf.mode=HTML5

受保护资源

@RestController
@RequestMapping(value = "/order")
public class TestController {

    @RequestMapping(value = "/demo")
    @ResponseBody
    public ResponseMessage getDemo() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        log.info(auth.toString());
        return ResponseMessage.success();
    }
}

应用启动类

@SpringBootApplication
@EnableAuthorizationServer
@EnableResourceServer
@MapperScan("org.net5ijy.oauth2.repository")
public class Oauth2Application {
    public static void main(String[] args) {
        // args = new String[] { "--debug" };
        SpringApplication.run(Oauth2Application.class, args);
    }
}

测试授权码模式

获取authorization_code授权码

使用浏览器访问:

http://localhost:7001/oauth/authorize?response_type=code&client_id=net5ijy&redirect_uri=http://localhost:8080&scope=all

地址:

http://localhost:7001/oauth/authorize

参数:

参数说明
response_typecode
client_id根据实际的client-id填写,此处写net5ijy
redirect_uri生成code后的回调地址,http://localhost:8080
scope权限范围

登录,admin001和123456:
在这里插入图片描述

允许授权:
在这里插入图片描述

看到浏览器重定向到了http://localhost:8080并携带了code参数,这个code就是授权服务器生成的授权码:
在这里插入图片描述

获取token令牌

使用curl命令获取token令牌:

curl --user net5ijy:123456 -X POST -d "grant_type=authorization_code&scope=all&redirect_uri=http%3a%2f%2flocalhost%3a8080&code=ubtvR4" http://localhost:7001/oauth/token

地址:

http://localhost:7001/oauth/token

参数:

参数说明
grant_type授权码模式,写authorization_code
scope权限范围
redirect_uri回调地址,http://localhost:8080需要urlencode
code就是上一步生成的授权码

在这里插入图片描述

返回值:

{
    "access_token": "c5836918-1924-4b0a-be67-043218c6e7e0",
    "token_type": "bearer",
    "refresh_token": "7950b7f9-7d60-41da-9a95-bd2c8b29ada1",
    "expires_in": 7199,
    "scope": "all"
}

这样就获取到了token令牌,该token的访问权限范围是all权限,在2小时后失效。

使用token访问资源

http://localhost:7001/order/demo?access_token=c5836918-1924-4b0a-be67-043218c6e7e0

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值