点击上方“芋道源码”,选择“设为星标”
管她前浪,还是后浪?
能浪的浪,才是好浪!
每天 10:33 更新文章,每天掉亿点点头发...
源码精品专栏
原创 | Java 2021 超神之路,很肝~
中文详细注释的开源项目
RPC 框架 Dubbo 源码解析
网络应用框架 Netty 源码解析
消息中间件 RocketMQ 源码解析
数据库中间件 Sharding-JDBC 和 MyCAT 源码解析
作业调度中间件 Elastic-Job 源码解析
分布式事务中间件 TCC-Transaction 源码解析
Eureka 和 Hystrix 源码解析
Java 并发源码
来源:阿Q说代码
简介
原理
实战
项目准备
引入依赖
SecurityConfig类
认证流程
鉴权流程
项目源码地址
作为一名后端开发人员,权限这个名词应该算是特别熟悉的了。就算是java里的类也有 public、private 等权限
之分。之前项目里一直使用shiro作为权限管理的框架。说实话,shiro的确挺强大的,但是它也有很多不好的地方。
Spring Security是Spring官方提供的安全框架,它具有能与Spring无缝整合、全面的权限控制、专门为web开发而设计等特点,比之shiro更加强大。今天我们就来聊聊如何使用Spring Security + JWT 来实现身份认证和用户授权。
简介
先赘述一下身份认证和用户授权:
用户认证(
Authentication
):系统通过校验用户提供的用户名和密码来验证该用户是否为系统中的合法主体,即是否可以访问该系统;用户授权(
Authorization
):系统为用户分配不同的角色,以获取对应的权限,即验证该用户是否有权限执行该操作;
Web
应用的安全性包括用户认证和用户授权两个部分,而Spring Security
(以下简称Security
)基于Spring
框架,正好可以完整解决该问题。
它的真正强大之处在于它可以轻松扩展以满足自定义要求。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
视频教程:https://doc.iocoder.cn/video/
原理
Security
可以看做是由一组filter
过滤器链组成的权限认证。它的整个工作流程如下所示:图中绿色认证方式是可以配置的,橘黄色和蓝色的位置不可更改:
FilterSecurityInterceptor
:最后的过滤器,它会决定当前的请求可不可以访问Controller
ExceptionTranslationFilter
:异常过滤器,接收到异常消息时会引导用户进行认证;
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/yudao-cloud
视频教程:https://doc.iocoder.cn/video/
实战
项目准备
我们使用Spring Boot
框架来集成。
1.pom
文件引入的依赖
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<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!-- 阿里JSON解析器 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.74</version> </dependency> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.10.6</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency>
2.application.yml
配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22spring: application: name: securityjwt datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/cheetah?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC username: root password: 123456 server: port: 8080 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.itcheetah.securityjwt.entity configuration: map-underscore-to-camel-case: true rsa: key: pubKeyFile: C:UsersDesktopjwtid_key_rsa.pub priKeyFile: C:UsersDesktopjwtid_key_rsa
3.SQL
文件
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/** * sys_user_info **/ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for sys_user_info -- ---------------------------- DROP TABLE IF EXISTS `sys_user_info`; CREATE TABLE `sys_user_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` 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, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1; /** * product_info **/ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for product_info -- ---------------------------- DROP TABLE IF EXISTS `product_info`; CREATE TABLE `product_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `price` decimal(10, 4) NULL DEFAULT NULL, `create_date` datetime(0) NULL DEFAULT NULL, `update_date` datetime(0) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;
引入依赖
1
2
3
4
5
6
7
8
9
10
11<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--Token生成与解析--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
引入之后启动项目,会有如图所示:

其中用户名为user
,密码为上图中的字符串。
SecurityConfig类
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//开启全局方法安全性 @EnableGlobalMethodSecurity(prePostEnabled=true, securedEnabled=true) public class SecurityConfig extends WebSecurityConfigurerAdapter { //认证失败处理类 @Autowired private AuthenticationEntryPointImpl unauthorizedHandler; //提供公钥私钥的配置类 @Autowired private RsaKeyProperties prop; @Autowired private UserInfoService userInfoService; @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity // CSRF禁用,因为不使用session .csrf().disable() // 认证失败处理类 .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() // 基于token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 过滤请求 .authorizeRequests() .antMatchers( HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js" ).permitAll() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated() .and() .headers().frameOptions().disable(); // 添加JWT filter httpSecurity.addFilter(new TokenLoginFilter(super.authenticationManager(), prop)) .addFilter(new TokenVerifyFilter(super.authenticationManager(), prop)); } //指定认证对象的来源 public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userInfoService) //从前端传递过来的密码就会被加密,所以从数据库 //查询到的密码必须是经过加密的,而这个过程都是 //在用户注册的时候进行加密的。 .passwordEncoder(passwordEncoder()); } //密码加密 @Bean public BCryptPasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
「拦截规则」
anyRequest
:匹配所有请求路径access
:SpringEl
表达式结果为true
时可以访问anonymous
:匿名可以访问denyAll
:用户不能访问fullyAuthenticated
:用户完全认证可以访问(非remember-me
下自动登录)hasAnyAuthority
:如果有参数,参数表示权限,则其中任何一个权限可以访问hasAnyRole
:如果有参数,参数表示角色,则其中任何一个角色可以访问hasAuthority
:如果有参数,参数表示权限,则其权限可以访问hasIpAddress
:如果有参数,参数表示IP
地址,如果用户IP
和参数匹配,则可以访问hasRole
:如果有参数,参数表示角色,则其角色可以访问permitAll
:用户可以任意访问rememberMe
:允许通过remember-me
登录的用户访问authenticated
:用户登录后可访问
认证失败处理类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/** * 返回未授权 */ @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable { private static final long serialVersionUID = -8970718410437077606L; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException { int code = HttpStatus.UNAUTHORIZED; String msg = "认证失败,无法访问系统资源,请先登陆"; ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg))); } }
认证流程
自定义认证过滤器
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
72public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { private AuthenticationManager authenticationManager; private RsaKeyProperties prop; public TokenLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) { this.authenticationManager = authenticationManager; this.prop = prop; } /** * @author cheetah * @description 登陆验证 * @date 2021/6/28 16:17 * @Param [request, response] * @return org.springframework.security.core.Authentication **/ public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { UserPojo sysUser = new ObjectMapper().readValue(request.getInputStream(), UserPojo.class); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword()); return authenticationManager.authenticate(authRequest); }catch (Exception e){ try { response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); PrintWriter out = response.getWriter(); Map resultMap = new HashMap(); resultMap.put("code", HttpServletResponse.SC_UNAUTHORIZED); resultMap.put("msg", "用户名或密码错误!"); out.write(new ObjectMapper().writeValueAsString(resultMap)); out.flush(); out.close(); }catch (Exception outEx){ outEx.printStackTrace(); } throw new RuntimeException(e); } } /** * @author cheetah * @description 登陆成功回调 * @date 2021/6/28 16:17 * @Param [request, response, chain, authResult] * @return void **/ public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { UserPojo user = new UserPojo(); user.setUsername(authResult.getName()); user.setRoles((List<RolePojo>)authResult.getAuthorities()); //通过私钥进行加密:token有效期一天 String token = JwtUtils.generateTokenExpireInMinutes(user, prop.getPrivateKey(), 24 * 60); response.addHeader("Authorization", "Bearer "+token); try { response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpServletResponse.SC_OK); PrintWriter out = response.getWriter(); Map resultMap = new HashMap(); resultMap.put("code", HttpServletResponse.SC_OK); resultMap.put("msg", "认证通过!"); resultMap.put("token", token); out.write(new ObjectMapper().writeValueAsString(resultMap)); out.flush(); out.close(); }catch (Exception outEx){ outEx.printStackTrace(); } } }
流程
Security
默认登录路径为/login
,当我们调用该接口时,它会调用上边的attemptAuthentication
方法;




所以我们要自定义UserInfoService
继承UserDetailsService
实现loadUserByUsername
方法;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public interface UserInfoService extends UserDetailsService { } @Service @Transactional public class UserInfoServiceImpl implements UserInfoService { @Autowired private SysUserInfoMapper userInfoMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserPojo user = userInfoMapper.queryByUserName(username); return user; } }
其中的loadUserByUsername
返回的是UserDetails
类型,所以UserPojo
继承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@Data public class UserPojo implements UserDetails { private Integer id; private String username; private String password; private Integer status; private List<RolePojo> roles; @JsonIgnore @Override public Collection<? extends GrantedAuthority> getAuthorities() { //理想型返回 admin 权限,可自已处理这块 List<SimpleGrantedAuthority> auth = new ArrayList<>(); auth.add(new SimpleGrantedAuthority("ADMIN")); return auth; } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.username; } /** * 账户是否过期 **/ @JsonIgnore @Override public boolean isAccountNonExpired() { return true; } /** * 是否禁用 */ @JsonIgnore @Override public boolean isAccountNonLocked() { return true; } /** * 密码是否过期 */ @JsonIgnore @Override public boolean isCredentialsNonExpired() { return true; } /** * 是否启用 */ @JsonIgnore @Override public boolean isEnabled() { return true; } }
当认证通过之后会在SecurityContext
中设置Authentication
对象,回调调用successfulAuthentication
方法返回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
28public class TokenVerifyFilter extends BasicAuthenticationFilter { private RsaKeyProperties prop; public TokenVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) { super(authenticationManager); this.prop = prop; } public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String header = request.getHeader("Authorization"); if (header == null || !header.startsWith("Bearer ")) { //如果携带错误的token,则给用户提示请登录! chain.doFilter(request, response); } else { //如果携带了正确格式的token要先得到token String token = header.replace("Bearer ", ""); //通过公钥进行解密:验证tken是否正确 Payload<UserPojo> payload = JwtUtils.getInfoFromToken(token, prop.getPublicKey(), UserPojo.class); UserPojo user = payload.getUserInfo(); if(user!=null){ UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(user.getUsername(), null, user.getAuthorities()); //将认证信息存到安全上下文中 SecurityContextHolder.getContext().setAuthentication(authResult); chain.doFilter(request, response); } } } }
当我们访问时需要在header
中携带token
信息至于文中
JWT
生成token
和RSA
生成公钥、私钥的部分,可在源码中查看。
项目源码地址
https://gitee.com/zhangxiaoQ/security-jwt
欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:
已在知识星球更新源码解析如下:
最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。
提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。
获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。
1
2文章有帮助的话,在看,转发吧。 谢谢支持哟 (*^__^*)
最后
以上就是孝顺钢铁侠最近收集整理的关于再见了 shiro !的全部内容,更多相关再见了内容请搜索靠谱客的其他文章。
发表评论 取消回复