SpringSecurity + oauth2 + jwt
|字数总计:4.6k|阅读时长:22分钟|阅读量:
SpringSecurity + oauth2 + jwt
代码从黑马程序员学成在线项目抽出来自己再修改的喔!
github:https://github.com/ishuaige/springsecuritydemo
gitee:https://gitee.com/niumazlb/springsecuritydemo
0.准备环境
① 数据库准备
数据库设计得比较简单,真正的权限控制需要 权限表、角色表、角色权限表、用户表、用户角色表
ddl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(0) NOT NULL AUTO_INCREMENT, `name` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '管理员姓名', `password` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '管理员密码', `role` bit(1) NULL DEFAULT NULL COMMENT '角色:1--超级管理员 0---用户', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
INSERT INTO `user` VALUES (1, 'admin', '123456', b'1'); INSERT INTO `user` VALUES (2, 'admin1', '123456', b'0'); INSERT INTO `user` VALUES (3, 'admin2', '123', b'1'); INSERT INTO `user` VALUES (4, 'admin3', '123123', NULL);
|
② 开发环境准备
项目总体结构
(1) 父工程
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.8</version> <relativePath/> </parent> <groupId>com.niuma</groupId> <artifactId>springsecuritydemo</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring-cloud.version>2021.0.0</spring-cloud.version> <mysql-connector-java.version>8.0.30</mysql-connector-java.version> <mybatis-plus-boot-starter.version>3.5.2</mybatis-plus-boot-starter.version> <fastjson.version>1.2.83</fastjson.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql-connector-java.version}</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus-boot-starter.version}</version> </dependency> </dependencies> </dependencyManagement>
</project>
|
(2) 认证模块 auth
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.niuma</groupId> <artifactId>springsecuritydemo</artifactId> <version>1.0-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <groupId>com.niuma</groupId> <artifactId>auth</artifactId> <version>0.0.1-SNAPSHOT</version> <name>auth</name> <description>auth</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-security</artifactId> <version>2.2.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> <version>2.2.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency>
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </dependency>
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build>
</project>
|
application.yml
spring.cloud.compatibility-verifier.enabled设置为
false
禁用兼容性验证机制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| server: port: 8089 servlet: context-path: /auth spring: mvc: pathmatch: matching-strategy: ant_path_matcher cloud: compatibility-verifier: enabled: false datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/tea_db username: root password: 123456 mybatis-plus: configuration: map-underscore-to-camel-case: false log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: isDelete logic-delete-value: 1 logic-not-delete-value: 0
|
(3) 资源模块 demo-R
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.niuma</groupId> <artifactId>springsecuritydemo</artifactId> <version>1.0-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <groupId>com.demo</groupId> <artifactId>demoR</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo-R</name> <description>demo-R</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-security</artifactId> <version>2.2.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> <version>2.2.5.RELEASE</version> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </dependency>
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
</project>
|
application.yml
1 2 3 4 5 6
| server: port: 8080 spring: cloud: compatibility-verifier: enabled: false
|
1.认证模块
当引入了 Spring Security 之后,模块就被 Spring Security 管理了,我们需要进行一些个性化的配置,让他校验我们的规则。
〇 配置类
1.Security 安全管理配置
在项目中添加如下配置就可以实现对资源权限规则设定:
这是最终的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| package com.niuma.auth.config;
@EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired @Lazy DaoAuthenticationProviderCustom daoAuthenticationProviderCustom;
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(daoAuthenticationProviderCustom); }
@Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
@Bean public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/r/**").authenticated() .anyRequest().permitAll() .and() .formLogin().successForwardUrl("/login-success");
http.logout().logoutUrl("/logout");
}
}
|
2.授权服务器配置
注意这里配置的参数,请求要和它一致噢
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| package com.niuma.auth.config;
@Configuration @EnableAuthorizationServer public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Resource(name = "authorizationServerTokenServicesCustom") private AuthorizationServerTokenServices authorizationServerTokenServices;
@Autowired private AuthenticationManager authenticationManager;
@Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("niuma") .secret("niuma")
.resourceIds("niumaR") .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token") .scopes("all") .autoApprove(false) .redirectUris("http://localhost:8089") ; }
@Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { endpoints .authenticationManager(authenticationManager) .tokenServices(authorizationServerTokenServices) .allowedTokenEndpointRequestMethods(HttpMethod.POST); }
@Override public void configure(AuthorizationServerSecurityConfigurer security) { security .tokenKeyAccess("permitAll()") .checkTokenAccess("permitAll()") .allowFormAuthenticationForClients() ; }
}
|
3.Token 配置
配置 Token 的类型和生成策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| package com.niuma.auth.config;
@Configuration public class TokenConfig {
@Autowired @Lazy TokenStore tokenStore; private String SIGNING_KEY = "niuma";
@Autowired @Lazy private JwtAccessTokenConverter accessTokenConverter;
@Bean public TokenStore tokenStore() { return new JwtTokenStore(accessTokenConverter()); }
@Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey(SIGNING_KEY); return converter; }
@Bean(name = "authorizationServerTokenServicesCustom") public AuthorizationServerTokenServices tokenService() { DefaultTokenServices service = new DefaultTokenServices(); service.setSupportRefreshToken(true); service.setTokenStore(tokenStore);
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter)); service.setTokenEnhancer(tokenEnhancerChain);
service.setAccessTokenValiditySeconds(7200); service.setRefreshTokenValiditySeconds(259200); return service; }
}
|
4.自定义检验规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package com.niuma.auth.config;
@Slf4j @Component public class DaoAuthenticationProviderCustom extends DaoAuthenticationProvider {
@Autowired public void setUserDetailsService(UserDetailsService userDetailsService) { super.setUserDetailsService(userDetailsService); }
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
}
}
|
① 修改用户信息校验规则
我们要从自己的数据库拿数据来校验,并且添加权限
1.自定义校验参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package com.niuma.ucenter.model.dto;
import lombok.Data;
import java.util.HashMap; import java.util.Map;
@Data public class AuthParamsDto {
private String username; private String password; private String cellphone; private String checkCode; private String checkCodeKey; private String authType;
private Map<String, Object> payload = new HashMap<>();
}
|
2.实现 UserDetailsService
实现 UserDetailsService 接口并实现 loadUserByUsername
loadUserByUsername 方法就是校验登录参数的
为了拓展更多的校验方法(账号密码登录,微信登录。。),我们并不写死校验逻辑,而是通过 IOC 容器拿到对应的校验方法类进行校验
loadUserByUsername 方法的参数就是请求中的 username 参数,但是我们让它拓展成我们需要的参数
最终返回一个 UserDetails 对象,里面包括了用户信息和权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| package com.niuma.ucenter.service.impl;
@Service @Slf4j public class UserDetailsServiceImpl implements UserDetailsService {
@Resource UserMapper userMapper;
@Resource ApplicationContext applicationContext;
@Override public UserDetails loadUserByUsername(String info) throws UsernameNotFoundException { AuthParamsDto authParamsDto = null; try { authParamsDto = JSON.parseObject(info, AuthParamsDto.class); } catch (Exception e) { log.info("认证请求不符合项目要求:{}", info); throw new RuntimeException("认证请求数据格式不对"); } String authType = authParamsDto.getAuthType(); AuthService authService = applicationContext.getBean(authType + "_authservice", AuthService.class); com.niuma.ucenter.model.domain.User user = authService.execute(authParamsDto); return getUserPrincipal(user); }
public UserDetails getUserPrincipal(com.niuma.ucenter.model.domain.User user) { List<String> permissionList = new ArrayList<>();
int role = user.getRole(); if(role == 1){ permissionList.add("admin"); }else { permissionList.add("teacher"); } if (permissionList.size() == 0) { permissionList.add("test"); }
String[] authorities = permissionList.toArray(new String[0]); user.setPassword(null); String jsonString = JSON.toJSONString(user); UserDetails userDetails = User.withUsername(jsonString).password("").authorities(authorities).build();
return userDetails; }
}
|
3.认证方式的接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package com.niuma.ucenter.service;
import com.niuma.ucenter.model.domain.User; import com.niuma.ucenter.model.dto.AuthParamsDto;
public interface AuthService {
User execute(AuthParamsDto authParamsDto);
}
|
3.1 账号密码认证的实现类
service 注入 bean 必须按照规定命名,保证在 UserDetailsServiceImpl 中可以通过 ioc 拿到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| package com.niuma.ucenter.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.niuma.ucenter.mapper.UserMapper; import com.niuma.ucenter.model.domain.User; import com.niuma.ucenter.model.dto.AuthParamsDto; import com.niuma.ucenter.service.AuthService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service;
@Slf4j @Service("password_authservice") public class PasswordAuthServiceImpl implements AuthService {
@Autowired UserMapper userMapper;
@Autowired PasswordEncoder passwordEncoder;
@Override public User execute(AuthParamsDto authParamsDto) {
String username = authParamsDto.getUsername(); User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getName, username)); if (user == null) { throw new RuntimeException("账号不存在"); } String passwordDB = user.getPassword(); String passwordInput = authParamsDto.getPassword(); boolean matches = passwordEncoder.matches(passwordInput, passwordDB); if (!matches) { throw new RuntimeException("账号或密码错误"); }
return user; } }
|
2.资源模块
当你微服务中的一个资源模块需要引入权限管理时,先在 pom 中添加 Spring Security 和 Oauth2 依赖。
〇 配置类
1.资源服务配置
我们需要声明我们是一个资源
注意这里的资源服务标识要跟前面的认证服务配置类里面指定的一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| package com.niuma.demor.config;
@Configuration @EnableResourceServer @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
public static final String RESOURCE_ID = "niumaR";
@Autowired TokenStore tokenStore;
@Override public void configure(ResourceServerSecurityConfigurer resources) { resources.resourceId(RESOURCE_ID) .tokenStore(tokenStore) .stateless(true); }
@Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/r/**").authenticated() .anyRequest().permitAll() ; }
}
|
2.Token 配置
注意这里的配置要和前面的保持统一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| package com.niuma.demor.config;
@Configuration public class TokenConfig { String SIGNING_KEY = "niuma";
@Autowired @Lazy private JwtAccessTokenConverter accessTokenConverter;
@Bean public TokenStore tokenStore() { return new JwtTokenStore(accessTokenConverter()); }
@Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey(SIGNING_KEY); return converter; }
}
|
① 测试接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package com.niuma.demor.controller;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController;
@RestController public class RController {
@GetMapping("/r/getR1") public String GetR1(){ return "这是资源服务的R1"; }
@GetMapping("/r/getR2") @PreAuthorize("hasAuthority('admin')") public String GetR2(){ return "这是资源服务的R2,只有admin才能访问喔"; } }
|
3.测试
个人想法:认证模块只是一个用来获取 Token 的地方,后续前端只需要拿着 token 访问资源就完事了
这里并没有对 jwt 做校验,一般这个工作是由网关做统一校验后分发到其他微服务的,后续有机会再做吧。
① 访问认证服务获得 token
执行得到 token
看看 token 存的东西,user_name 包含的就是用户信息的 json
② 访问资源服务
注意,需要在请求头中加上 Authorization 参数喔
Authorization 参数值格式:Bearer (注意后面有个空格)
(1)执行获取资源请求
获取 R1 资源成功,因为没有指定权限
获取 R2 资源不成功,因为在 R2 接口那里我们配置了只有 admin 权限可以请求