本文在第一篇简单例子的基础上进行了升级,本文例子只做认证授权服务器。使用场景的话:该认证授权服务器可做多个客户端的认证中心,多个客户端都访问该服务获取token,然后通过token访问接口。客户端部分下一篇讲解。
升级部分包括:
- 用户信息存在于Mysql中,方便管理用户;
- Token存在于Redis中,方便管理token,如果存在于内存中,重启服务token就失效了;
运用技术:springboot2.x、springsecurity5.x、mybatis、redis等
1.pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- spring-security-oauth2 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
<!--redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Druid 数据连接池依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
<!-- lombok插件 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
<scope>provided</scope>
</dependency>
</dependencies>
2.application.yml 配置
server:
port: 8888
spring:
redis:
host: localhost
port: 6379
password: 123456
database: 0
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 20
min-idle: 0
timeout: 2000
datasource:
url: jdbc:mysql://localhost:3307/oauth?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: Wbb2018.
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
security:
oauth2:
client:
redirectUris: http://localhost:9999/webjars/springfox-swagger-ui/o2c.html,http://localhost:9998/webjars/springfox-swagger-ui/o2c.html #该配置目前没有用上,会在下一篇客户端中讲解
clientId: demoClient
clientSecret: demoSecret
authorizedGrantTypes: authorization_code,client_credentials, password, refresh_token,implicit
scopes: read,write
resourceIds: oauth2-resource
accessTokenValiditySeconds: 3600
refreshTokenValiditySeconds: 3600
mybatis:
configuration:
map-underscore-to-camel-case: true
3.Sql方面的代码
3.1表结构sql代码。
这里我建了3个表,user表用于存放用户信息(账号,密码),role表用于存放role权限信息,user_permission用于存放用户和权限的绑定信息。
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(64) DEFAULT NULL,
`describe` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('1', 'USER', '普通用户');
INSERT INTO `role` VALUES ('2', 'ADMIN', '管理员');
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(64) DEFAULT NULL,
`password` varchar(64) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'user', '123456');
INSERT INTO `user` VALUES ('2', 'admin', '123456');
-- ----------------------------
-- Table structure for user_permission
-- ----------------------------
DROP TABLE IF EXISTS `user_permission`;
CREATE TABLE `user_permission` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`role_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user_permission
-- ----------------------------
INSERT INTO `user_permission` VALUES ('1', '1', '1');
INSERT INTO `user_permission` VALUES ('2', '2', '2');
3.2.JavaBean
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class RoleDO {
private Integer id;
private String name;
private String describe;
}
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UserDO {
private Integer id;
private String username;
private String password;
private String roleName;
}
3.3.Mybatis Mapper ,都比较简单,就不做解释
import com.wbb.security.bean.RoleDO;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
@Repository
public interface RoleMapper {
@Select("select id,name,describe from role where id = #{id}")
RoleDO selectById(@Param("id") Integer id);
}
import com.wbb.security.bean.UserDO;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
@Repository
public interface UserMapper {
@Select("select id,username,password from user where username = #{username}")
UserDO selectByName(@Param("username") String username);
}
import com.wbb.security.bean.UserPermissionDO;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UserPermissionMapper {
@Select("select up.id,up.role_id,up.user_id,r.`name` as role_name from user_permission up " +
"join user u on up.user_id = u.id " +
"join role r on up.role_id = r.id where user_id = #{userId}")
List<UserPermissionDO> selectByUserId(Integer userId);
}
4.配置认证授权服务器,就不详细介绍,需要介绍可以去看上一篇
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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.approval.UserApprovalHandler;
import org.springframework.security.oauth2.provider.token.TokenStore;
@Configuration
@EnableAuthorizationServer
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
TokenStore tokenStore;
@Autowired
private UserApprovalHandler userApprovalHandler;
@Value("${spring.security.oauth2.client.clientId}")
private String clientId;
@Value("${spring.security.oauth2.client.clientSecret}")
private String clientSecret;
@Value("${spring.security.oauth2.client.redirectUris}")
private String[] redirectUris;
@Value("${spring.security.oauth2.client.authorizedGrantTypes}")
private String[] authorizedGrantTypes;
@Value("${spring.security.oauth2.client.resourceIds}")
private String resourceIds;
@Value("${spring.security.oauth2.client.scopes}")
private String[] scopes;
@Value("${spring.security.oauth2.client.accessTokenValiditySeconds}")
private int accessTokenValiditySeconds;
@Value("${spring.security.oauth2.client.refreshTokenValiditySeconds}")
private int refreshTokenValiditySeconds;
/**
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.realm(resourceIds)
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();
}
/**
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient(clientId)
.secret(new BCryptPasswordEncoder().encode(clientSecret))
.redirectUris(redirectUris)
.authorizedGrantTypes(authorizedGrantTypes)
.scopes(scopes)
.resourceIds(resourceIds)
.accessTokenValiditySeconds(accessTokenValiditySeconds)
.refreshTokenValiditySeconds(refreshTokenValiditySeconds);
}
/**
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 这里与上一篇不同的是,增加tokenStore(用户存放token)和userApprovalHandler(用于用户登录)配置
endpoints.tokenStore(tokenStore).userApprovalHandler(userApprovalHandler)
endpoints.tokenStore(tokenStore)
.userApprovalHandler(userApprovalHandler)
.authenticationManager(authenticationManager)
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);//获取token允许的访问方式
}
}
5.配置
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.TokenApprovalStore;
import org.springframework.security.oauth2.provider.approval.TokenStoreUserApprovalHandler;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
/**
* Spring Security默认是禁用注解的,要想开启注解, 需要在继承WebSecurityConfigurerAdapter,并在类上加@EnableGlobalMethodSecurity注解
* 这里使用redis存储token
*/
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Autowired
private ClientDetailsService clientDetailsService;
//使用redis来存储token信息
@Bean
public TokenStore tokenStore(){
return new RedisTokenStore(redisConnectionFactory);
}
//MyUserDetailsService用于存放用户信息
@Bean
public MyUserDetailsService myUserDetailsService(){
return new MyUserDetailsService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService())
.passwordEncoder(passwordEncoder());
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
.and().logout().logoutUrl("/logout").deleteCookies("JSESSIONID").permitAll()
.and().authorizeRequests()
.antMatchers("/login.html").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
/**
* spring5.x 需要注入一个PasswordEncoder,否则会报这个错There is no PasswordEncoder mapped for the id \"null\"
* 加了密码加密,所有用到的密码都需要这个加密方法加密
* @return
*/
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 需要配置这个支持password模式
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
@Autowired
public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore){
TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
handler.setTokenStore(tokenStore);
handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
handler.setClientDetailsService(clientDetailsService);
return handler;
}
@Bean
@Autowired
public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {
TokenApprovalStore store = new TokenApprovalStore();
store.setTokenStore(tokenStore);
return store;
}
}
- 编写MyUserDetailsService类,实现UserDetailsService接口,在loadUserByUsername()方法中来做自己的认证登录逻辑。在该方法中先根据用户名去数据库中找该用户,找到该用户后,再去找该用户的权限,最后通过User类的构造方法返回一个User对象。
import com.wbb.security.bean.UserDO;
import com.wbb.security.bean.UserPermissionDO;
import com.wbb.security.mapper.RoleMapper;
import com.wbb.security.mapper.UserMapper;
import com.wbb.security.mapper.UserPermissionMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.ArrayList;
import java.util.List;
public class MyUserDetailsService implements UserDetailsService {
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
UserMapper userMapper;
@Autowired
RoleMapper roleMapper;
@Autowired
UserPermissionMapper userPermissionMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("login name:"+username);
UserDO userDO =userMapper.selectByName(username);
if(userDO == null){
throw new UsernameNotFoundException("用户不存在");
}
ArrayList<GrantedAuthority> authorities = new ArrayList<>();
List<UserPermissionDO> userPermissionDOs = userPermissionMapper.selectByUserId(userDO.getId());
for(UserPermissionDO userPermissionDO:userPermissionDOs){
//这个权限牵涉到底层的投票机制,默认是一票制AffirmativeBased:如果有任何一个投票器运行访问,请求将被立刻允许,而不管之前可能有的拒绝决定
// RoleVoter投票器识别以"ROLE_"为前缀的role,这里配置已ROLE_前缀开头的role
authorities.add(new SimpleGrantedAuthority("ROLE_"+userPermissionDO.getRoleName()));
}
return new User(username,passwordEncoder.encode(userDO.getPassword()),authorities);
}
}
7.超low登陆页面login.html,在本篇中无用,配合下一篇客户端使用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" value="登录">
</form>
</body>
</html>
8.区别于上一篇,由于本文讲解的是认证授权服务端,就不配置ResourceServerConfig。配置完上面几步后,认证服务器就已经完成了。
接下来,启动项目,直接访问http://localhost:8888/oauth/token?grant_type=password&client_id=demoClient&client_secret=demoSecret&username=admin&password=123456,访问成功,返回值也没问题
换一个user用户,同样没问题
演示到这里就结束了,因为是认证服务器,本文就不访问接口url了,只演示获取token这部分,url访问放到下一篇客户端中演示。
讲到这里,本篇文章到此也就结束了,如果文章中有问题,或者有一些不够严谨完善的地方,希望大家体谅体谅。欢迎大家留言,交流交流。
最后附上本项目github地址