我是靠谱客的博主 忧伤羽毛,这篇文章主要介绍整合SpringSecurity和JWT实现登录认证和授权前言一、SpringSecurity是什么?二、JWT是什么?三、整合步骤四、编写注册、登录方法总结,现在分享给大家,希望可以做个参考。

文章目录

  • 前言
  • 一、SpringSecurity是什么?
  • 二、JWT是什么?
    • 1. JWT的组成
    • 2. JWT实现认证和授权的原理
  • 三、整合步骤
    • 1. 引入相关依赖
    • 2. yml配置中加入jwt配置信息
    • 3. 添加JWT token的工具类
    • 4. 添加SpringSecurity的配置类
    • 5. 添加自定义结果处理类
      • 5.1 添加CustomAccessDeniedHandler
      • 5.2 添加CustomAuthenticationEntryPoint
    • 6. 添加JwtUser类
    • 7. 添加过滤器JwtAuthenticationTokenFilter
  • 四、编写注册、登录方法
    • 1. 编写controller
    • 2. 编写service
    • 3. 编写service实现
    • 4. 测试
    • 4.1 测试注册用户接口
    • 4.2 测试登录接口
    • 4.3 测试其它接口
    • 4.4 测试访问需要权限的接口
  • 总结


前言

前后端分离的开发模式中,接口的认证与授权是重中之重。由于接口都是无状态的,那势必就需要一套认证和授权框架来解决“你是谁”以及“你能干什么”的问题,所以,这篇文章就来聊一聊如何整合SpringSecurity和JWT实现登录认证和授权。
源码传送门:https://gitee.com/huoqstudy/xiliu-admin.git


一、SpringSecurity是什么?

SpringSecurity是一个强大的可高度定制的认证和授权框架,对于Spring应用来说它是一套Web安全标准。SpringSecurity注重于为Java应用提供认证和授权功能,像所有的Spring项目一样,它对自定义需求具有强大的扩展性。
其核心就是一组过滤器链,在spring security中一种过滤器处理一种认证方式,项目启动后将会自动配置
在这里插入图片描述

二、JWT是什么?

JWT是JSON WEB TOKEN的缩写,它是基于 RFC 7519 标准定义的一种可以安全传输的的JSON对象,由于使用了数字签名,所以是可信任和安全的。总结来说,JWT只是一个生成token的机制。

1. JWT的组成

JWT token的格式:header.payload.signature
可以在该网站上获得解析结果:https://jwt.io/
在这里插入图片描述

  • header中用于存放签名的生成算法

  • payload中用于存放用户名、token的生成时间和过期时间

  • signature为以header和payload生成的签名,一旦header和payload被篡改,验证将失败

2. JWT实现认证和授权的原理

  • 用户调用登录接口,登录成功后获取到JWT的token;
  • 之后用户每次调用接口都在http的header中添加一个叫Authorization的头,值为JWT的token;
  • 后台程序通过对Authorization头中信息的解码及数字签名校验来获取其中的用户信息,从而实现认证和授权。

三、整合步骤

1. 引入相关依赖

代码如下:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
<!--SpringSecurity依赖配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--JWT(Json Web Token)登录支持--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>

2. yml配置中加入jwt配置信息

复制代码
1
2
3
4
5
6
jwt: tokenHeader: X-Token #JWT存储的请求头 tokenHead: Bearer #令牌前缀 secret: yz-admin-secret #JWT加解密使用的密钥 expiration: 604800 #JWT的超期限时间秒(60*60*24)

3. 添加JWT token的工具类

用于生成和解析JWT 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
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
@Slf4j @Component public class JwtTokenUtil { private static final String CLAIM_KEY_USERNAME = "sub"; private static final String CLAIM_KEY_CREATED = "created"; @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expire; /** * 从token中获取登录用户名 */ public String getUserNameFromToken(String token) { String username; try { Claims claims = getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null; } return username; } /** * 校验token */ public boolean validateToken(String token, UserDetails userDetails) { String username = getUserNameFromToken(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } /** * 根据用户信息生成token */ public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } /** * 判断token是否已经失效 */ private boolean isTokenExpired(String token) { Date expiredDate = getClaimsFromToken(token).getExpiration(); return expiredDate.before(new Date()); } private String generateToken(Map<String, Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) //签名算法 .signWith(SignatureAlgorithm.HS512, secret) .compact(); } /** * 生成token的过期时间 */ private Date generateExpirationDate() { return new Date(System.currentTimeMillis() + expire * 1000); } private Claims getClaimsFromToken(String token) { Claims claims = null; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } catch (Exception e) { log.info("JWT格式验证失败:{}",token); } return claims; } }

4. 添加SpringSecurity的配置类

spring security和spring mvc做了很好的集成,一共只需要做两件事,给web配置类加上@EanbleWebSecurity,继承WebSecurityConfigurerAdapter定义个性化配置。
这里我们在加上一个全局的注解@EnableGlobalMethodSecurity(prePostEnabled=true),该注解会在方法执行前进行验证。

configure(HttpSecurity httpSecurity):用于配置需要拦截的url路径、jwt过滤器及出异常后的处理器;
configure(AuthenticationManagerBuilder auth):用于配置UserDetailsService及PasswordEncoder;
UserDetailsService: SpringSecurity定义的核心接口,用于根据用户名获取用户信息,需要自行实现;
UserDetails:SpringSecurity定义用于封装用户信息的类(主要是用户信息和权限),需要自行实现;
PasswordEncoder:SpringSecurity定义的用于对密码进行编码及比对的接口,目前使用的是BCryptPasswordEncoder;
JwtAuthenticationTokenFilter:在用户名和密码校验前添加的过滤器,如果有jwt的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
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
85
86
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled=true) public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomAccessDeniedHandler customAccessDeniedHandler; @Autowired private CustomAuthenticationEntryPoint customAuthenticationEntryPoint; @Autowired private XlUserService xlUserService; @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.csrf().disable()// 由于使用的是JWT,我们这里不需要csrf .sessionManagement()// 基于token,所以不需要session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 允许对于网站静态资源的无授权访问 .antMatchers(HttpMethod.GET, "/", "/*.html", "/favicon.ico", "/**/*.html", "/**/*.css", "/**/*.js", "/swagger-resources/**", "/v2/api-docs/**" ) .permitAll() // 对登录注册要允许匿名访问 .antMatchers("/ucenter/xl-user/login", "/ucenter/xl-user/register") .permitAll() //跨域请求会先进行一次options请求 .antMatchers(HttpMethod.OPTIONS) .permitAll() .anyRequest()// 除上面外的所有请求全部需要鉴权认证 .authenticated(); // 禁用缓存 httpSecurity.headers().cacheControl(); // 添加JWT filter httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class); //添加自定义未授权和未登录结果返回 httpSecurity.exceptionHandling() .accessDeniedHandler(customAccessDeniedHandler) .authenticationEntryPoint(customAuthenticationEntryPoint); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()) .passwordEncoder(passwordEncoder()); } @Bean @Override public UserDetailsService userDetailsService() { //获取登录用户信息 return username -> { XlUser user = xlUserService.getUserByCode(username); if (user != null) { List<XlResource> permissionList = xlUserService.getResourceList(user.getUserId()); return new JwtUser(user,permissionList); } throw new UsernameNotFoundException("用户名或密码错误"); }; } /** * 装载BCrypt密码编码器 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * JWT filter */ @Bean public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){ return new JwtAuthenticationTokenFilter(); } }

5. 添加自定义结果处理类

5.1 添加CustomAccessDeniedHandler

当用户没有访问权限时的处理器,用于返回JSON格式的处理结果(若配置了全局异常的需要注意,会导致该处理类失效。需要在全局异常类里面在单独处理AccessDeniedException异常 );

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
@Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json"); httpServletResponse.getWriter().println(JSONUtil.parse(R.error(ResultCodeEnum.FORBIDDEN.getCode(),ResultCodeEnum.FORBIDDEN.getMessage()))); httpServletResponse.getWriter().flush(); } }

5.2 添加CustomAuthenticationEntryPoint

当未登录或token失效时,返回JSON格式的结果。

复制代码
1
2
3
4
5
6
7
8
9
10
11
@Component public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json"); httpServletResponse.getWriter().println(JSONUtil.parse(R.error(ResultCodeEnum.UNAUTHORIZED.getCode(),ResultCodeEnum.UNAUTHORIZED.getMessage()))); httpServletResponse.getWriter().flush(); } }

6. 添加JwtUser类

Spring Security需要我们实现几个东西,第一个是UserDetails:这个接口中规定了用户的几个必须要有的方法,所以我们创建一个JwtUser类来实现这个接口。为什么不直接使用User类?因为这个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
public class JwtUser implements UserDetails { private XlUser user; private List<XlResource> resourceList; public JwtUser(XlUser user, List<XlResource> resourceList) { this.user = user; this.resourceList = resourceList; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { //返回当前用户的权限 return resourceList.stream() .map(resource ->new SimpleGrantedAuthority(resource.getResourceId()+":"+resource.getResourceName())) .collect(Collectors.toList()); } @Override public String getPassword() { return user.getPassWord(); } @Override public String getUsername() { return user.getUserCode(); } /** * 账户是否未过期 **/ @Override public boolean isAccountNonExpired() { return true; } /** * 账户是否未锁定 **/ @Override public boolean isAccountNonLocked() { return true; } /** * 密码是否未过期 **/ @Override public boolean isCredentialsNonExpired() { return true; } /** * 账户是否激活 **/ @Override public boolean isEnabled() { return user.getStatus().equals(1); } }

7. 添加过滤器JwtAuthenticationTokenFilter

用户除登录之外的请求,都要求必须携带JWT Token。所以我们需要另外一个Filter对这些请求做一个拦截。这个拦截器主要是提取header中的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
@Slf4j public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { String authHeader = httpServletRequest.getHeader(this.tokenHeader); if (StringUtils.isNotBlank(authHeader) && authHeader.startsWith(this.tokenHead)) { String authToken = authHeader.substring(this.tokenHead.length()); String username = jwtTokenUtil.getUserNameFromToken(authToken); log.info("checking authentication " + username); if (StringUtils.isNotBlank(username) && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); // 校验token if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails( httpServletRequest)); log.info("authenticated user " + username + ", setting security context"); SecurityContextHolder.getContext().setAuthentication(authentication); } } } filterChain.doFilter(httpServletRequest, httpServletResponse); } }

四、编写注册、登录方法

1. 编写controller

复制代码
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
@Api(tags = "后台用户管理") @RestController @RequestMapping("/ucenter/xl-user") public class XlUserController { @Autowired private XlUserService xlUserService; @Value("${jwt.tokenHead}") private String tokenHead; @ApiOperation(value = "注册用户") @PostMapping(value = "register") public R register(@RequestBody XlUser user) { XlUser xlUser = xlUserService.register(user); if (xlUser == null) { return R.error("注册失败,用户名已存在"); } return R.ok(xlUser); } @ApiOperation(value = "登录") @PostMapping(value = "login") public R login(@RequestBody LoginVo loginVo) { String token = xlUserService.login(loginVo.getUserName(), loginVo.getPassWord()); if (StringUtils.isBlank(token)) { return R.error("用户名或密码错误"); } Map<String, String> tokenMap = new HashMap<>(); tokenMap.put("token", token); tokenMap.put("tokenHead", tokenHead); return R.ok(tokenMap); } @ApiOperation("获取用户所有可访问的资源") @GetMapping(value = "/resource/{userId}") public R getResourceList(@PathVariable Long userId) { List<XlResource> permissionList = xlUserService.getResourceList(userId); return R.ok(permissionList); } }

2. 编写service

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface XlUserService extends IService<XlUser> { /** * 注册功能 * @param user * @return XlUser */ XlUser register(XlUser user); /** * 登录功能 * @param username 用户名 * @param password 密码 * @return 生成的JWT的token */ String login(String username, String password); /** * 获取用户所有可访问的资源 * @param userId 用户id * @return list */ List<XlResource> getResourceList(Long userId);

3. 编写service实现

复制代码
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
@Slf4j @Service public class XlUserServiceImpl extends ServiceImpl<XlUserMapper, XlUser> implements XlUserService { @Autowired private PasswordEncoder passwordEncoder; @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Override public XlUser register(XlUser user) { XlUser newXlUser = new XlUser(); BeanUtils.copyProperties(user, newXlUser); //查询是否有相同用户名的用户 List<XlUser> xlUsers = this.baseMapper.selectList(new LambdaQueryWrapper<XlUser>().eq(XlUser::getUserCode,newXlUser.getUserCode())); if (CollectionUtil.isNotEmpty(xlUsers)) { return null; } //将密码进行加密操作 String encodePassword = passwordEncoder.encode(user.getPassWord()); newXlUser.setPassWord(encodePassword); this.baseMapper.insert(newXlUser); return newXlUser; } @Override public String login(String username, String password) { String token = null; try { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (!passwordEncoder.matches(password, userDetails.getPassword())) { throw new BadCredentialsException("密码不正确"); } UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); token = jwtTokenUtil.generateToken(userDetails); } catch (AuthenticationException e) { log.warn("登录异常:{}", e.getMessage()); } return token; } @Override public List<XlResource> getResourceList(Long userId) { return this.baseMapper.getResourceList(userId); }

4. 测试

编写好相关代码后,重启项目进行测试。注册和登录接口因为配置了允许匿名访问,所以可以直接访问。而其他接口则做了登录认证,所以访问的时候会校验。

4.1 测试注册用户接口

注册成功
在这里插入图片描述

4.2 测试登录接口

登录成功
在这里插入图片描述

4.3 测试其它接口

测试需要登录的接口,未登录前访问接口,提示未登录
在这里插入图片描述
若已登录,则需要在请求头添加token参数,添加好之后再次请求,则请求成功。(ps:若knife4j 在线文档调试没有请求头部设置,则需要在 文档管理-》个性化设置 开启动态请求参数,刷新即可。)
在这里插入图片描述
在这里插入图片描述

4.4 测试访问需要权限的接口

在接口上增加一个权限配置,由于登录的账号没有配置任何的权限,访问该接口时,即时登录了也访问不了,提示无权限,如下图所示:在这里插入图片描述
在这里插入图片描述


总结

感谢大家的阅读,以上就是今天要讲的内容。本文简单介绍了如何整合SpringSecurity和JWT实现登录认证和授权,如有不足之处,纯属能力有限,还望不吝赐教。若觉得对你有帮助的话,还不忘点赞评论支持一波哟~

最后

以上就是忧伤羽毛最近收集整理的关于整合SpringSecurity和JWT实现登录认证和授权前言一、SpringSecurity是什么?二、JWT是什么?三、整合步骤四、编写注册、登录方法总结的全部内容,更多相关整合SpringSecurity和JWT实现登录认证和授权前言一、SpringSecurity是什么内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(96)

评论列表共有 0 条评论

立即
投稿
返回
顶部