基于Springboot集成security、oauth2实现认证鉴权、资源管理
1、Oauth2简介
OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0。
2、Oauth2服务器
- 授权服务器 Authorization Service.
- 资源服务器 Resource Service.
授权服务器
授权服务器,即服务提供商专门用来处理认证的服务器。在这里简单说一下,主要的功能;
1、通过请求获得令牌(Token),默认的URL是/oauth/token.
2、根据令牌(Token)获取相应的权限.
资源服务器
资源服务器托管了受保护的用户账号信息,并且对接口资源进行用户权限分配及管理,简单的说,就是某个接口(/user/add),我限制只能持有管理员权限的用户才能访问,那么普通用户就没有访问的权限。
以下摘自百度百科图:
3、Demo实战加代码详解
前面我是简单地介绍了一下oauth2的一些基本概念,关于oauth2的深入介绍,可以去搜索更多其它相关oauth2的博文,在这里推荐一篇前辈的博文https://www.cnblogs.com/Wddpct/p/8976480.html,里面有详细的oauth2介绍,包括原理、实现流程等都讲得比较详细。我的课题,是主要是以实战为主,理论的东西我不想介绍太多, 这里是我个人去根据自己的业务需求去改造的,存在很多可优化的点,希望大家可以指出和给予我一些宝贵意见。
接下来开始介绍我的代码流程吧!
准备
新建一个springboot项目,引入以下依赖。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency> <!--web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency> <!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency> <!--sl4f日志框架-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</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>
<version>2.3.3.RELEASE</version>
</dependency> <!--JPA数据库持久化-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency> <!--json工具-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
</dependencies>
项目目录结构
接口
这里我只编写了一个AuthController,里面基本所有关于用户管理及登录、注销的接口我都定义出来了。
AuthController代码如下:
package com.unionman.springbootsecurityauth2.controller; import com.unionman.springbootsecurityauth2.dto.LoginUserDTO;
import com.unionman.springbootsecurityauth2.dto.UserDTO;
import com.unionman.springbootsecurityauth2.service.RoleService;
import com.unionman.springbootsecurityauth2.service.UserService;
import com.unionman.springbootsecurityauth2.utils.AssertUtils;
import com.unionman.springbootsecurityauth2.vo.ResponseVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import javax.validation.Valid; /**
* @description 用户权限管理
* @author Zhifeng.Zeng
* @date 2019/4/19 13:58
*/
@Slf4j
@Validated
@RestController
@RequestMapping("/auth/")
public class AuthController { @Autowired
private UserService userService; @Autowired
private RoleService roleService; @Autowired
private RedisTokenStore redisTokenStore; /**
* @description 添加用户
* @param userDTO
* @return
*/
@PostMapping("user")
public ResponseVO add(@Valid @RequestBody UserDTO userDTO){
userService.addUser(userDTO);
return ResponseVO.success();
} /**
* @description 删除用户
* @param id
* @return
*/
@DeleteMapping("user/{id}")
public ResponseVO deleteUser(@PathVariable("id")Integer id){
userService.deleteUser(id);
return ResponseVO.success();
} /**
* @descripiton 修改用户
* @param userDTO
* @return
*/
@PutMapping("user")
public ResponseVO updateUser(@Valid @RequestBody UserDTO userDTO){
userService.updateUser(userDTO);
return ResponseVO.success();
} /**
* @description 获取用户列表
* @return
*/
@GetMapping("user")
public ResponseVO findAllUser(){
return userService.findAllUserVO();
} /**
* @description 用户登录
* @param loginUserDTO
* @return
*/
@PostMapping("user/login")
public ResponseVO login(LoginUserDTO loginUserDTO){
return userService.login(loginUserDTO);
} /**
* @description 用户注销
* @param authorization
* @return
*/
@GetMapping("user/logout")
public ResponseVO logout(@RequestHeader("Authorization") String authorization){
redisTokenStore.removeAccessToken(AssertUtils.extracteToken(authorization));
return ResponseVO.success();
} /**
* @description 用户刷新Token
* @param refreshToken
* @return
*/
@GetMapping("user/refresh/{refreshToken}")
public ResponseVO refresh(@PathVariable(value = "refreshToken") String refreshToken){
return userService.refreshToken(refreshToken);
} /**
* @description 获取所有角色列表
* @return
*/
@GetMapping("role")
public ResponseVO findAllRole(){
return roleService.findAllRoleVO();
} }
这里所有的接口功能,我都已经在业务代码里实现了,后面相关登录、注销、及刷新token的等接口的业务实现的内容我会贴出来。接下来我需要讲解的是关于oath2及security的详细配置。
注意一点:这里没有角色的增删改功能,只有获取角色列表功能,为了节省时间,我这里的角色列表是项目初始化阶段,直接生成的固定的两个角色,分别是ROLE_USER(普通用户)、ROLE_ADMIN(管理员);同时初始化一个默认的管理员。
springbootsecurityauth.sql脚本如下:
SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;
/**
初始化角色信息
*/
CREATE TABLE IF NOT EXISTS `um_t_role`(
`id` INT(11) PRIMARY KEY AUTO_INCREMENT ,
`description` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`created_time` BIGINT(20) NOT NULL,
`name` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`role` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL
);
INSERT IGNORE INTO `um_t_role`(id,`name`,description,created_time,role) VALUES(1,'管理员','管理员拥有所有接口操作权限',UNIX_TIMESTAMP(NOW()),'ADMIN'),(2,'普通用户','普通拥有查看用户列表与修改密码权限,不具备对用户增删改权限',UNIX_TIMESTAMP(NOW()),'USER'); /**
初始化一个默认管理员
*/
CREATE TABLE IF NOT EXISTS `um_t_user`(
`id` INT(11) PRIMARY KEY AUTO_INCREMENT ,
`account` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`description` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`name` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL
);
INSERT IGNORE INTO `um_t_user`(id,account,`password`,`name`,description) VALUES(1,'admin','','小小丰','系统默认管理员'); /**
关联表赋值
*/
CREATE TABLE IF NOT EXISTS `um_t_role_user`(
`role_id` INT(11),
`user_id` INT(11)
);
INSERT IGNORE INTO `um_t_role_user`(role_id,user_id)VALUES(1,1);
配置
application.yml文件:
server:
port: 8080
spring:
# mysql 配置
datasource:
url: jdbc:mysql://localhost:3306/auth_test?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: 123456
schema: classpath:springbootsecurityauth.sql
sql-script-encoding: utf-8
initialization-mode: always
driver-class-name: com.mysql.jdbc.Driver
# 初始化大小,最小,最大
initialSize: 1
minIdle: 3
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 30000
validationQuery: select 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打开PSCache,并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,slf4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
#redis 配置
redis:
open: true # 是否开启redis缓存 true开启 false关闭
database: 1
host: localhost
port: 6379
timeout: 5000s # 连接超时时长(毫秒)
jedis:
pool:
max-active: 8 #连接池最大连接数(使用负值表示没有限制)
max-idle: 8 #连接池中的最大空闲连接
max-wait: -1s #连接池最大阻塞等待时间(使用负值表示没有限制)
min-idle: 0 #连接池中的最小空闲连接 # jpa 配置
jpa:
database: mysql
show-sql: false
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5Dialect
资源服务器与授权服务器
编写类Oauth2Config,实现资源服务器与授权服务器,这里的资源服务器与授权服务器以内部类的形式实现。
Oauth2Config代码如下:
package com.unionman.springbootsecurityauth2.config; import com.unionman.springbootsecurityauth2.handler.CustomAuthExceptionHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
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.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
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.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; import java.util.concurrent.TimeUnit; /**
* @author Zhifeng.Zeng
* @description OAuth2服务器配置
*/
@Configuration
public class OAuth2Config { public static final String ROLE_ADMIN = "ADMIN";
//访问客户端密钥
public static final String CLIENT_SECRET = "123456";
//访问客户端ID
public static final String CLIENT_ID ="client_1";
//鉴权模式
public static final String GRANT_TYPE[] = {"password","refresh_token"}; /**
* @description 资源服务器
*/
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Autowired
private CustomAuthExceptionHandler customAuthExceptionHandler; @Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.stateless(false)
.accessDeniedHandler(customAuthExceptionHandler)
.authenticationEntryPoint(customAuthExceptionHandler);
} @Override
public void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
//请求权限配置
.authorizeRequests()
//下边的路径放行,不需要经过认证
.antMatchers("/oauth/*", "/auth/user/login").permitAll()
//OPTIONS请求不需要鉴权
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
//用户的增删改接口只允许管理员访问
.antMatchers(HttpMethod.POST, "/auth/user").hasAnyAuthority(ROLE_ADMIN)
.antMatchers(HttpMethod.PUT, "/auth/user").hasAnyAuthority(ROLE_ADMIN)
.antMatchers(HttpMethod.DELETE, "/auth/user").hasAnyAuthority(ROLE_ADMIN)
//获取角色 权限列表接口只允许系统管理员及高级用户访问
.antMatchers(HttpMethod.GET, "/auth/role").hasAnyAuthority(ROLE_ADMIN)
//其余接口没有角色限制,但需要经过认证,只要携带token就可以放行
.anyRequest()
.authenticated(); }
} /**
* @description 认证授权服务器
*/
@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { @Autowired
private AuthenticationManager authenticationManager; @Autowired
private RedisConnectionFactory connectionFactory; @Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
String finalSecret = "{bcrypt}" + new BCryptPasswordEncoder().encode(CLIENT_SECRET);
//配置客户端,使用密码模式验证鉴权
clients.inMemory()
.withClient(CLIENT_ID)
//密码模式及refresh_token模式
.authorizedGrantTypes(GRANT_TYPE[0], GRANT_TYPE[1])
.scopes("all")
.secret(finalSecret);
} @Bean
public RedisTokenStore redisTokenStore() {
return new RedisTokenStore(connectionFactory);
} /**
* @description token及用户信息存储到redis,当然你也可以存储在当前的服务内存,不推荐
* @param endpoints
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
//token信息存到服务内存
/*endpoints.tokenStore(new InMemoryTokenStore())
.authenticationManager(authenticationManager);*/ //token信息存到redis
endpoints.tokenStore(redisTokenStore()).authenticationManager(authenticationManager);
//配置TokenService参数
DefaultTokenServices tokenService = new DefaultTokenServices();
tokenService.setTokenStore(endpoints.getTokenStore());
tokenService.setSupportRefreshToken(true);
tokenService.setClientDetailsService(endpoints.getClientDetailsService());
tokenService.setTokenEnhancer(endpoints.getTokenEnhancer());
//1小时
tokenService.setAccessTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(1));
//1小时
tokenService.setRefreshTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(1));
tokenService.setReuseRefreshToken(false);
endpoints.tokenServices(tokenService);
} @Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
//允许表单认证
oauthServer.allowFormAuthenticationForClients().tokenKeyAccess("isAuthenticated()")
.checkTokenAccess("permitAll()");
}
}
}
这里有个点要强调一下,就是上面的CustomAuthExceptionHandler ,这是一个自定义返回异常处理。要知道oauth2在登录时用户密码不正确或者权限不足时,oauth2内部携带的Endpoint处理,会默认返回401并且携带的message是它内部默认的英文,例如像这种:
感觉就很不友好,所以我这里自己去处理AuthException并返回自己想要的数据及数据格式给客户端。
CustomAuthExceptionHandler代码如下:
package com.unionman.humancar.handler; import com.alibaba.fastjson.JSON;
import com.unionman.humancar.enums.ResponseEnum;
import com.unionman.humancar.vo.ResponseVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException; /**
* @author Zhifeng.Zeng
* @description 自定义未授权 token无效 权限不足返回信息处理类
* @date 2019/3/4 15:49
*/
@Component
@Slf4j
public class CustomAuthExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { Throwable cause = authException.getCause();
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
// CORS "pre-flight" request
response.addHeader("Access-Control-Allow-Origin", "*");
response.addHeader("Cache-Control","no-cache");
response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
response.addHeader("Access-Control-Max-Age", "1800");
if (cause instanceof InvalidTokenException) {
log.error("InvalidTokenException : {}",cause.getMessage());
//Token无效
response.getWriter().write(JSON.toJSONString(ResponseVO.error(ResponseEnum.ACCESS_TOKEN_INVALID)));
} else {
log.error("AuthenticationException : NoAuthentication");
//资源未授权
response.getWriter().write(JSON.toJSONString(ResponseVO.error(ResponseEnum.UNAUTHORIZED)));
} } @Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.addHeader("Access-Control-Allow-Origin", "*");
response.addHeader("Cache-Control","no-cache");
response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
response.addHeader("Access-Control-Max-Age", "1800");
//访问资源的用户权限不足
log.error("AccessDeniedException : {}",accessDeniedException.getMessage());
response.getWriter().write(JSON.toJSONString(ResponseVO.error(ResponseEnum.INSUFFICIENT_PERMISSIONS)));
}
}
Spring Security
这里security主要承担的角色是,用户资源管理,简单地说就是,在客户端发送登录请求的时候,security会将先去根据用户输入的用户名和密码,去查数据库,如果匹配,那么就把相应的用户信息进行一层转换,然后交给认证授权管理器,然后认证授权管理器会根据相应的用户,给他分发一个token(令牌),然后下次进行请求的时候,携带着该token(令牌),认证授权管理器就能根据该token(令牌)去找到相应的用户了。
SecurityConfig代码如下:
package com.unionman.springbootsecurityauth2.config; import com.unionman.springbootsecurityauth2.domain.CustomUserDetail;
import com.unionman.springbootsecurityauth2.entity.User;
import com.unionman.springbootsecurityauth2.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.client.RestTemplate; import java.util.List; /**
* @description Security核心配置
* @author Zhifeng.Zeng
*/
@Configuration
@EnableWebSecurity
@Slf4j
public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired
private UserRepository userRepository; @Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
} @Bean
public RestTemplate restTemplate(){
return new RestTemplate();
} @Bean
@Override
protected UserDetailsService userDetailsService() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
return new UserDetailsService(){
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("username:{}",username);
User user = userRepository.findUserByAccount(username);
if(user != null){
CustomUserDetail customUserDetail = new CustomUserDetail();
customUserDetail.setUsername(user.getAccount());
customUserDetail.setPassword("{bcrypt}"+bCryptPasswordEncoder.encode(user.getPassword()));
List<GrantedAuthority> list = AuthorityUtils.createAuthorityList(user.getRole().getRole());
customUserDetail.setAuthorities(list);
return customUserDetail;
}else {//返回空
return null;
} }
};
} @Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
业务逻辑
这里我只简单地实现了用户的增删改查以及用户登录的业务逻辑。并没有做太深的业务处理,主要是重点看一下登录的业务逻辑。里面引了几个组件,简单说一下,RestTemplate(http客户端)用于发送http请求,ServerConfig(服务配置)用于获取本服务的ip和端口,RedisUtil(redis工具类) 用户对redis进行缓存的增删改查操作。
UserServiceImpl代码如下:
package com.unionman.springbootsecurityauth2.service.impl; import com.unionman.springbootsecurityauth2.config.ServerConfig;
import com.unionman.springbootsecurityauth2.domain.Token;
import com.unionman.springbootsecurityauth2.dto.LoginUserDTO;
import com.unionman.springbootsecurityauth2.dto.UserDTO;
import com.unionman.springbootsecurityauth2.entity.Role;
import com.unionman.springbootsecurityauth2.entity.User;
import com.unionman.springbootsecurityauth2.enums.ResponseEnum;
import com.unionman.springbootsecurityauth2.enums.UrlEnum;
import com.unionman.springbootsecurityauth2.repository.UserRepository;
import com.unionman.springbootsecurityauth2.service.RoleService;
import com.unionman.springbootsecurityauth2.service.UserService;
import com.unionman.springbootsecurityauth2.utils.BeanUtils;
import com.unionman.springbootsecurityauth2.utils.RedisUtil;
import com.unionman.springbootsecurityauth2.vo.LoginUserVO;
import com.unionman.springbootsecurityauth2.vo.ResponseVO;
import com.unionman.springbootsecurityauth2.vo.RoleVO;
import com.unionman.springbootsecurityauth2.vo.UserVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate; import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit; import static com.unionman.springbootsecurityauth2.config.OAuth2Config.CLIENT_ID;
import static com.unionman.springbootsecurityauth2.config.OAuth2Config.CLIENT_SECRET;
import static com.unionman.springbootsecurityauth2.config.OAuth2Config.GRANT_TYPE; @Service
public class UserServiceImpl implements UserService { @Autowired
private UserRepository userRepository; @Autowired
private RoleService roleService; @Autowired
private RestTemplate restTemplate; @Autowired
private ServerConfig serverConfig; @Autowired
private RedisUtil redisUtil; @Override
@Transactional(rollbackFor = Exception.class)
public void addUser(UserDTO userDTO) {
User userPO = new User();
User userByAccount = userRepository.findUserByAccount(userDTO.getAccount());
if(userByAccount != null){
//此处应该用自定义异常去返回,在这里我就不去具体实现了
try {
throw new Exception("This user already exists!");
} catch (Exception e) {
e.printStackTrace();
}
}
userPO.setCreatedTime(System.currentTimeMillis());
//添加用户角色信息
Role rolePO = roleService.findById(userDTO.getRoleId());
userPO.setRole(rolePO);
BeanUtils.copyPropertiesIgnoreNull(userDTO,userPO);
userRepository.save(userPO);
} @Override
@Transactional(rollbackFor = Exception.class)
public void deleteUser(Integer id) {
User userPO = userRepository.findById(id).get();
if(userPO == null){
//此处应该用自定义异常去返回,在这里我就不去具体实现了
try {
throw new Exception("This user not exists!");
} catch (Exception e) {
e.printStackTrace();
}
}
userRepository.delete(userPO);
} @Override
@Transactional(rollbackFor = Exception.class)
public void updateUser(UserDTO userDTO) {
User userPO = userRepository.findById(userDTO.getId()).get();
if(userPO == null){
//此处应该用自定义异常去返回,在这里我就不去具体实现了
try {
throw new Exception("This user not exists!");
} catch (Exception e) {
e.printStackTrace();
}
}
BeanUtils.copyPropertiesIgnoreNull(userDTO, userPO);
//修改用户角色信息
Role rolePO = roleService.findById(userDTO.getRoleId());
userPO.setRole(rolePO);
userRepository.saveAndFlush(userPO);
} @Override
public ResponseVO<List<UserVO>> findAllUserVO() {
List<User> userPOList = userRepository.findAll();
List<UserVO> userVOList = new ArrayList<>();
userPOList.forEach(userPO->{
UserVO userVO = new UserVO();
BeanUtils.copyPropertiesIgnoreNull(userPO,userVO);
RoleVO roleVO = new RoleVO();
BeanUtils.copyPropertiesIgnoreNull(userPO.getRole(),roleVO);
userVO.setRole(roleVO);
userVOList.add(userVO);
});
return ResponseVO.success(userVOList);
} @Override
public ResponseVO login(LoginUserDTO loginUserDTO) {
MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
paramMap.add("client_id", CLIENT_ID);
paramMap.add("client_secret", CLIENT_SECRET);
paramMap.add("username", loginUserDTO.getAccount());
paramMap.add("password", loginUserDTO.getPassword());
paramMap.add("grant_type", GRANT_TYPE[0]);
Token token = null;
try {
//因为oauth2本身自带的登录接口是"/oauth/token",并且返回的数据类型不能按我们想要的去返回
//但是我的业务需求是,登录接口是"user/login",由于我没研究过要怎么去修改oauth2内部的endpoint配置
//所以这里我用restTemplate(HTTP客户端)进行一次转发到oauth2内部的登录接口,比较简单粗暴
token = restTemplate.postForObject(serverConfig.getUrl() + UrlEnum.LOGIN_URL.getUrl(), paramMap, Token.class);
LoginUserVO loginUserVO = redisUtil.get(token.getValue(), LoginUserVO.class);
if(loginUserVO != null){
//登录的时候,判断该用户是否已经登录过了
//如果redis里面已经存在该用户已经登录过了的信息
//我这边要刷新一遍token信息,不然,它会返回上一次还未过时的token信息给你
//不便于做单点维护
token = oauthRefreshToken(loginUserVO.getRefreshToken());
redisUtil.deleteCache(loginUserVO.getAccessToken());
}
} catch (RestClientException e) {
try {
e.printStackTrace();
//此处应该用自定义异常去返回,在这里我就不去具体实现了
//throw new Exception("username or password error");
} catch (Exception e1) {
e1.printStackTrace();
}
}
//这里我拿到了登录成功后返回的token信息之后,我再进行一层封装,最后返回给前端的其实是LoginUserVO
LoginUserVO loginUserVO = new LoginUserVO();
User userPO = userRepository.findUserByAccount(loginUserDTO.getAccount());
BeanUtils.copyPropertiesIgnoreNull(userPO, loginUserVO);
loginUserVO.setPassword(userPO.getPassword());
loginUserVO.setAccessToken(token.getValue());
loginUserVO.setAccessTokenExpiresIn(token.getExpiresIn());
loginUserVO.setAccessTokenExpiration(token.getExpiration());
loginUserVO.setExpired(token.isExpired());
loginUserVO.setScope(token.getScope());
loginUserVO.setTokenType(token.getTokenType());
loginUserVO.setRefreshToken(token.getRefreshToken().getValue());
loginUserVO.setRefreshTokenExpiration(token.getRefreshToken().getExpiration());
//存储登录的用户
redisUtil.set(loginUserVO.getAccessToken(),loginUserVO,TimeUnit.HOURS.toSeconds(1));
return ResponseVO.success(loginUserVO);
} /**
* @description oauth2客户端刷新token
* @param refreshToken
* @date 2019/03/05 14:27:22
* @author Zhifeng.Zeng
* @return
*/
private Token oauthRefreshToken(String refreshToken) {
MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
paramMap.add("client_id", CLIENT_ID);
paramMap.add("client_secret", CLIENT_SECRET);
paramMap.add("refresh_token", refreshToken);
paramMap.add("grant_type", GRANT_TYPE[1]);
Token token = null;
try {
token = restTemplate.postForObject(serverConfig.getUrl() + UrlEnum.LOGIN_URL.getUrl(), paramMap, Token.class);
} catch (RestClientException e) {
try {
//此处应该用自定义异常去返回,在这里我就不去具体实现了
throw new Exception(ResponseEnum.REFRESH_TOKEN_INVALID.getMessage());
} catch (Exception e1) {
e1.printStackTrace();
}
}
return token;
} }
示例
这里我使用postman(接口测试工具)去对接口做一些简单的测试。
(1)这里我去发送一个获取用户列表的请求:
结果可以看到,由于没有携带token信息,所以返回了如下信息。
(2)接下来,我们先去登录。
登录成功后,这里会返回一系列信息,记住这个token信息,待会我们尝试使用这个token信息再次请求上面那个获取用户列表接口。
(3)携带token去获取用户列表
可以看到,可以成功拿到接口返回的资源(用户的列表信息)啦。
(4)这里测试一下,用户注销的接口。用户注销,会把redis里的token信息全部清除。
可以看到,注销成功了。那么我们再用这个已经被注销的token再去请求一遍那个获取用户列表接口。
很显然,此时已经报token无效了。
接下来,我们对角色的资源分配管理进行一个测试。可以看到我们库里面,项目初始化的时候,就已经创建了一个管理员,我们上面配置已经规定,管理员是拥有所有接口的访问权限的,而普通用户却只有查询权限。我们现在就来测试一下这个效果。
(1)首先我使用该管理员去添加一个普通用户。
可以看到,我们返回了添加成功信息了,那么我去查看一下用户列表。
很显然,现在这个用户已经成功添加进去了。
(2)接下来,我们用新添加的用户去登录一下该系统。
该用户也登录成功了,我们先保存这个token。
(3)我们现在携带着刚才登录的普通用户"小王"的token去添加一个普通用户。
可以看到,由于"小王"是普通用户,所以是不具备添加用户的权限的。
(4)那么我们现在用"小王"这个用户去查询一下用户列表。
可以看到,"小王"这个普通用户是拥有查询用户列表接口的权限的。
总结
基于Springboot集成security、oauth2实现认证鉴权、资源管理的博文就到这了。描述得其实已经较为详细了,具体代码的示例也给了相关的注释。基本上都是以最简单最基本的方式去做的一个整合Demo。一般实际应用场景里,业务会比较复杂,其中还会有,修改密码,重置密码,主动延时token时长,加密解密等等。这些就根据自己的业务需求去做相应的处理了,基本上的操作都是针对redis去做,因为token相关信息都是存储在redis的。
具体源码我已经上传到github:https://github.com/githubzengzhifeng/springboot-security-oauth2
基于Springboot集成security、oauth2实现认证鉴权、资源管理的更多相关文章
- springboot+spring security +oauth2.0 demo搭建(password模式)(认证授权端与资源服务端分离的形式)
项目security_simple(认证授权项目) 1.新建springboot项目 这儿选择springboot版本我选择的是2.0.6 点击finish后完成项目的创建 2.引入maven依赖 ...
- Spring Security OAuth2.0认证授权四:分布式系统认证授权
Spring Security OAuth2.0认证授权系列文章 Spring Security OAuth2.0认证授权一:框架搭建和认证测试 Spring Security OAuth2.0认证授 ...
- Spring Security OAuth2.0认证授权五:用户信息扩展到jwt
历史文章 Spring Security OAuth2.0认证授权一:框架搭建和认证测试 Spring Security OAuth2.0认证授权二:搭建资源服务 Spring Security OA ...
- Spring Security 接口认证鉴权入门实践指南
目录 前言 SpringBoot 示例 SpringBoot pom.xml SpringBoot application.yml SpringBoot IndexController SpringB ...
- Spring Security OAuth2.0认证授权三:使用JWT令牌
Spring Security OAuth2.0系列文章: Spring Security OAuth2.0认证授权一:框架搭建和认证测试 Spring Security OAuth2.0认证授权二: ...
- springboot集成shiro实现权限认证
github:https://github.com/peterowang/shiro 基于上一篇:springboot集成shiro实现身份认证 1.加入UserController package ...
- Spring Cloud Security OAuth2.0 认证授权系列(一) 基础概念
世界上最快的捷径,就是脚踏实地,本文已收录[架构技术专栏]关注这个喜欢分享的地方. 前序 最近想搞下基于Spring Cloud的认证授权平台,总体想法是可以对服务间授权,想做一个基于Agent 的无 ...
- Spring Security OAuth2.0认证授权二:搭建资源服务
在上一篇文章[Spring Security OAuth2.0认证授权一:框架搭建和认证测试](https://www.cnblogs.com/kuangdaoyizhimei/p/14250374. ...
- SpringBoot集成security
本文就SpringBoot集成Security的使用步骤做出解释说明.
随机推荐
- koa+mysql+vue+socket.io全栈开发之前端篇
React 与 Vue 之间的对比,是前端的一大热门话题. vue 简易上手的脚手架,以及官方提供必备的基础组件,比如 vuex,vue-router,对新手真的比较友好:react 则把这些都交给社 ...
- Unix中的I/O模型
本文所指的I/O均是网络I/O. 一. POSIX对同步.异步I/O的定义 我们先大致看看POSIX对同步.异步的定义,不用细究,重点看我标红的部分就行. 同步I/O会导致请求进程阻塞,直到I/O操作 ...
- 开发教程(四) MIP组件平台使用说明
组件审核平台用于上传 MIP 组件.经过自动校验之后,提交审核,通过审核的组件会定时推送到线上,供网站使用. 平台地址:https://www.mipengine.org/platform/ 1. 使 ...
- ASP.Net Core Razor+AdminLTE 小试牛刀
AdminLTE 一个基于 bootstrap 的轻量级后台模板,这个前端界面个人感觉很清爽,对于一个大后端的我来说,可以减少较多的时间去承担前端的工作但又必须去独立去完成一个后台系统开发的任务,并且 ...
- Windows server 1709(不含UI)模板部署
1.系统安装 在虚拟机导入安装镜像,客户端操作系统选择” windows server 2012”,虚拟磁盘类型选择”SCSI”:依照安装向导正确安装操作系统 2.安装vmware tools 选择虚 ...
- winform 实现类似于TrackBar的自定义滑动条,功能更全
功能很全,随便列几个 1.可以设置滑块的大小,边框颜色.背景色.形状等等吧 2.可以设置轨道的方向.边框颜色.背景色.阴影等等 ... 效果图: 下载链接https://download.csdn.n ...
- vue组件-构成组件-父子组件相互传递数据
组件对于vue来说非常重要,学习学习了基础vue后,再回过头来把组件弄透! 一.概念 组件意味着协同工作,通常父子组件会是这样的关系:组件 A 在它的模版中使用了组件 B . 它们之间必然需要相互通信 ...
- c++ 之模板进阶
c++中的多态主要体现在模板与继承上. 继承可以理解为有相互关系的不同数据结构的集合. 而模板则是完全独立的数据结构,彼此无需依赖 在函数中使用模板, 可以根据函数传入的参数自动推导类型,从而省略到很 ...
- ASP.Net Core on Linux (CentOS7) 共享第三方依赖库部署
背景: 这周,心情来潮,想把 Aries 开发框架 和 Taurus 开发框架 给部署到Linux上,于是开始折腾了. 经过重重非人的坑,终于完成了任务: Aries on CentOS7:mvc.a ...
- redis发布订阅Java代码实现
Redis除了可以用作缓存数据外,另一个重要用途是它实现了发布订阅(pub/sub)消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息. 为了实现redis的发布订阅机制,首先要打开re ...