Spring Security
Spring Security 是 Spring Resource 社区的一个安全组件。在安全方面,有两个主要的领域,一是“认证”,即你是谁;二是“授权”,即你拥有什么权限,Spring Security 的主要目标就是在这两个领域
Spring OAuth2
OAuth2 是一个标准的授权协议,允许不同的客户端通过认证和授权的形式来访问被其保护起来的资源
OAuth2 协议在 Spring Resource 中的实现为 Spring OAuth2,Spring OAuth2 分为:OAuth2 Provider 和 OAuth2 Client
OAuth2 Provider
OAuth2 Provider 负责公开被 OAuth2 保护起来的资源
OAuth2 Provider 需要配置代表用户的 OAuth2 客户端信息,被用户允许的客户端就可以访问被 OAuth2 保护的资源。OAuth2 Provider 通过管理和验证 OAuth2 令牌来控制客户端是否有权限访问被其保护的资源
另外,OAuth2 Provider 还必须为用户提供认证 API 接口。根据认证 API 接口,用户提供账号和密码等信息,来确认客户端是否可以被 OAuth2 Provider 授权。这样做的好处就是第三方客户端不需要获取用户的账号和密码,通过授权的方式就可以访问被 OAuth2 保护起来的资源
OAuth2 Provider 的角色被分为 Authorization Service (授权服务) 和 Resource Service (资源服务),通常它们不在同一个服务中,可能一个 Authorization Service 对应多个 Resource Service
Spring OAuth2 需配合 Spring Security 一起使用,所有的请求由 Spring MVC 控制器处理,并经过一系列的 Spring Security 过滤器
在 Spring Security 过滤器链中有以下两个节点,这两个节点是向 Authorization Service 获取验证和授权的:
- 授权节点:默认为 /oauth/authorize
- 获取 Token 节点:默认为 /oauth/token
OAuth2 Client
OAuth2 Client (客户端) 用于访问被 OAuth2 保护起来的资源
新建 spring-security-oauth2-server
pom
1 | <parent> |
application.yml
1 | server: |
数据表
创建用户和角色及中间表:
1 | DROP TABLE IF EXISTS role; |
INSERT INTO role (name) VALUES (‘ROLE_USER’);
INSERT INTO role (name) VALUES (‘ROLE_ADMIN’);
INSERT INTO user (username, password) VALUES (‘test’, ‘123’);
INSERT INTO user_role (user_id, role_id) VALUES (1, 1);
1 |
|
@Entity
public class Role implements GrantedAuthority {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public String getAuthority() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString(){
return name;
}
}
1 |
|
@Entity
public class User implements UserDetails, Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column
private String password;
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
private List<Role> authorities;
public User(){
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public void setAuthorities(List<Role> authorities) {
this.authorities = authorities;
}
@Override
public boolean isAccountNonExpired(){
return true;
}
@Override
public boolean isAccountNonLocked(){
return true;
}
@Override
public boolean isCredentialsNonExpired(){
return true;
}
@Override
public boolean isEnabled(){
return true;
}
}
1 |
|
public interface UserDao extends JpaRepository<User, Long> {
User findByUsername(String username);
}
1 |
@Service
public class UserServiceDetail implements UserDetailsService {
@Autowired
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userDao.findByUsername(username);
}
}
1 |
|
@EnableEurekaClient
@SpringBootApplication
public class Oauth2ServerApp {
public static void main(String[] args){
SpringApplication.run(Oauth2ServerApp.class, args);
}
}
1 |
|
@Configuration
@EnableWebSecurity // 开启 Spring Security
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级别上的保护
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceDetail userServiceDetail;
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated() // 所有请求都需要安全验证
.and()
.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceDetail).passwordEncoder(passwordEncoder());
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
1 |
|
@Bean
public PasswordEncoder passwordEncoder() {
return new MyPasswordEncoder();
}
1 |
|
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return PasswordUtil.getencryptPassword((String)charSequence);
}
@Override
public boolean matches(CharSequence charSequence, String s) {
boolean result = PasswordUtil.matches(charSequence, s);
return result;
}
}
1 |
|
public class PasswordUtil {
private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
public static String getencryptPassword(String password){
return encoder.encode(password);
}
public static boolean matches(CharSequence rawPassword, String encodedPassword){
return encoder.matches(rawPassword, encodedPassword);
}
}
1 |
|
@Configuration
@EnableAuthorizationServer // 开启授权服务
@EnableResourceServer // 需要对外暴露获取和验证 Token 的接口,所以也是一个资源服务
public class OAuth2Config extends AuthorizationServerConfigurerAdapter{
@Autowired
private DataSource dataSource;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserServiceDetail userServiceDetail;
@Override
// 配置客户端信息
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() // 将客户端的信息存储在内存中
.withClient("browser") // 客户端 id, 需唯一
.authorizedGrantTypes("refresh_token", "password") // 认证类型为 refresh_token, password
.scopes("ui") // 客户端域
.and()
.withClient("eureka-client") // 另一个客户端
.secret("123456") // 客户端密码
.authorizedGrantTypes("client_credentials", "refresh_token", "password")
.scopes("server");
}
@Override
// 配置授权 token 的节点和 token 服务
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore()) // token 的存储方式
.authenticationManager(authenticationManager) // 开启密码验证,来源于 WebSecurityConfigurerAdapter
.userDetailsService(userServiceDetail); // 读取验证用户的信息
}
@Override
// 配置 token 节点的安全策略
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()") // 获取 token 的策略
.checkTokenAccess("isAuthenticated()");
}
@Bean
public TokenStore tokenStore() {
// return new InMemoryTokenStore();
return new JdbcTokenStore(dataSource);
}
}
1 |
|
@RestController
@RequestMapping(“/users”)
public class UserController {
@RequestMapping(value = "/current", method = RequestMethod.GET)
public Principal getUser(Principal principal){
return principal;
}
}
1 |
|
{
“access_token”: “5dc978ab-8c7e-4286-92f5-5655b8d15c98”,
“token_type”: “bearer”,
“refresh_token”: “7ef02b1c-6e8a-485f-adc9-18a48c2ae410”,
“expires_in”: 43199,
“scope”: “server”
}
1 |
|
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
1 |
|
security:
oauth2:
resource:
user-info-uri: http://localhost:8081/uaa/users/current
client:
client-id: eureka-client
client-secret: 123456
access-token-uri: http://localhost:8081/uaa/oauth/token
grant-type: client_credentials, password
scope: server
1 |
|
@Configuration
@EnableResourceServer // 开启资源服务
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级别上的保护
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/register").permitAll()
.anyRequest().authenticated();
}
}
1 |
|
@EnableOAuth2Client // 开启 OAuth2 Client
@EnableConfigurationProperties
@Configuration
public class OAuth2ClientConfig {
@Bean
@ConfigurationProperties(prefix = "security.oauth2.client")
// 配置受保护的资源信息
public ClientCredentialsResourceDetails clientCredentialsResourceDetails(){
return new ClientCredentialsResourceDetails();
}
@Bean
// 过滤器,存储当前请求和上下文
public RequestInterceptor oAuth2FeignRequestInterceptor(){
return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), clientCredentialsResourceDetails());
}
@Bean
public OAuth2RestTemplate clientCredentialsRestTemplate (){
return new OAuth2RestTemplate(clientCredentialsResourceDetails());
}
}
1 |
|
@RestController
@RequestMapping(“/user”)
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value = "/register", method = RequestMethod.POST)
public User createUser(@RequestParam("username") String username
, @RequestParam("password") String password){
return userService.create(username, password);
}
}
1 |
@RestController
public class HiController {
@Value("${server.port}")
int port;
@Value("${version}")
String version;
@GetMapping("/hi")
public String home(@RequestParam String name){
return "Hello " + name + ", from port: " + port + ", version: " + version;
}
@PreAuthorize("hasAuthority('ROLE_ADMIN')") // 需要权限
@RequestMapping("/hello")
public String hello(){
return "hello!";
}
}
1 |
|
{
“id”: 2,
“username”: “admin”,
“password”: “123”,
“authorities”: null,
“enabled”: true,
“accountNonExpired”: true,
“credentialsNonExpired”: true,
“accountNonLocked”: true
}
1 |
|
{
“access_token”: “dc4959fb-9ced-430e-9a78-5e9609c3baac”,
“token_type”: “bearer”,
“refresh_token”: “06cdcf55-fe6a-4367-94e1-051c2da86e37”,
“expires_in”: 43199,
“scope”: “server”
}
1 |
|
Hello Victor, from port: 8011, version: 1.0.2
1 |
|
{
“error”: “access_denied”,
“error_description”: “不允许访问”
}
1
2
3
手动授权:
INSERT INTO user_role (user_id, role_id) VALUES (2, 2);
1
2
3
重新获取 token 后再次访问接口
hello!
完整代码:[GitHub](https://github.com/VictorBu/code-snippet/tree/master/java/spring-cloud-parent)
**本人 C# 转 Java 的 newbie, 如有错误或不足欢迎指正,谢谢**