在生产环境中,对发在的API增加授权保护是非常必要的。JWT作为一个无状态的授权校捡技术,非常适合于分布式系统架构。服务器端不需要保存用户状态,因此,无须采用Redis等技术来实现各个服务节点之间共享Session数据。

  本节通过实例讲解如何用JWT技术进行授权认证和保护。

1.1 配置安全类

(1)自定义用户

查看代码

 package com.intehel.jwt.domain;

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Entity
@Data
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String username;
private String password;
private Boolean enabled;
private Boolean accountNonExpired;
private Boolean accountNonLocked;
private Boolean credentialsNonExpired;
@ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.PERSIST)
private List<Role> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
}

(2)自定义角色

查看代码

 package com.intehel.jwt.domain;

import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Data
@Entity(name = "role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private String nameZh;
}

(3)JPA

package com.intehel.jwt.repository;

import com.intehel.jwt.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User,Integer> {
User findUserByUsername(String username);
}
package com.intehel.jwt.repository;
import com.intehel.jwt.domain.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface UserRoleRepository extends JpaRepository<Role,Integer> {
Role findByName(String name);
}

(4)认证失败和认证成功处理器

查看代码

package com.intehel.jwt.handler;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.https.httpsServletRequest;
import javax.servlet.https.httpsServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
* @Author:李自航
* @Description:
* @CreateDate:2022/8/5 10:15
* @UPdateDate:2022/8/5 10:15
* @Version:版本号
*/

@Component
public class JwtAuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(httpsServletRequest request, httpsServletResponse response, AuthenticationException exception) throws IOException, ServletException {
request.setCharacterEncoding("UTF-8");
String name = request.getParameter("name");
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\n" +
"\t\"status\":\"error\",\n" +
"\t\"message\":\"用户名或密码错误\"\n" +
"}");
out.flush();
out.close();
}
}

查看代码

 package com.intehel.jwt.handler;


import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.https.httpsServletRequest;
import javax.servlet.https.httpsServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;

@Component
public class JwtAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(httpsServletRequest request, httpsServletResponse response, Authentication authentication) throws ServletException, IOException {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal != null && principal instanceof UserDetails){
UserDetails user = (UserDetails) principal;
request.getSession().setAttribute("userDetail",user);
String role = "";
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
for (GrantedAuthority authority : authorities){
role = authority.getAuthority();
}
String token = "灌水灌水";
response.setHeader("token",token);
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\n" +
"\t\"status\":\"ok\",\n" +
"\t\"message\":\"登录成功\"\n" +
"}\n");
out.flush();
out.close();
}

}
}

(5)配置安全类

package com.intehel.jwt.config;

import com.intehel.jwt.handler.JwtAuthenticationFailHandler;
import com.intehel.jwt.handler.JwtAuthenticationSuccessHandler;
import com.intehel.jwt.handler.MyAuthenticationFailureHandler;
import com.intehel.jwt.handler.MyAuthenticationSuccessHandler;
import com.intehel.jwt.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.httpsSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private JwtAuthenticationFailHandler myAuthenticationFailureHandler;
@Autowired
MyUserDetailsService jwtDetailsService;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Override
protected void configure(httpsSecurity https) throws Exception {
https.antMatcher("/jwt/**")
.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/doLogin")
.loginPage("/mylogin.html")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler)
.and()
.authorizeRequests()
.antMatchers("/register/mobile").permitAll()
.antMatchers("/article/**").authenticated()
.antMatchers("/jwt/tasks/**").hasRole("USER")
.anyRequest().authenticated()
.and()
.csrf().disable();
https.logout().permitAll();
https.cors().and().csrf().ignoringAntMatchers("/jwt/**");
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(jwtDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}

@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/jwt/register/mobile");

}
}

  从上面代码可以看出,此处JWT的安全配置和上面已经讲解过的安全配置并无区别,没有特别的参数需要配置。

1.2 自定义登录界面

查看代码

 <!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<style>
#login .container #login-row #login-column #login-box {
border: 1px solid #9c9c9c;
background-color: #EAEAEA;
}
</style>
<body>
<div id="login">
<div class="container">
<div id="login-row" class="row justify-content-center align-items-center">
<div id="login-column" class="col-md-6">
<div id="login-box" class="col-md-12">
<form id="login-form" class="form" action="/doLogin" method="post">
<h3 class="text-center text-info">登录</h3>
<!--/*@thymesVar id="SPRING_SECURITY_LAST_EXCEPTION" type="com"*/-->
<div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>
<div class="form-group">
<label for="username" class="text-info">用户名:</label><br>
<input type="text" name="username" id="username" class="form-control">
</div>
<div class="form-group">
<label for="password" class="text-info">密码:</label><br>
<input type="text" name="password" id="password" class="form-control">
</div>
<div class="form-group">
<input type="submit" name="submit" class="btn btn-info btn-md" value="登录">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

1.3 处理注册

在注册时为了安全,需要将注册的密码经过加密再写入数据库中。

   spring security 5之后,需要对密码添加这个类型(id),可参考文章www.cnblogs.com/majianming/p/7923604.html

  

查看代码

 package com.intehel.jwt.controller;

import com.intehel.jwt.domain.Role;
import com.intehel.jwt.domain.User;
import com.intehel.jwt.repository.UserRepository;
import com.intehel.jwt.repository.UserRoleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/jwt")
public class JwtUserController {
@Autowired
private UserRepository userRepository;
@Autowired
private UserRoleRepository userRoleRepository;
@RequestMapping(value = "/register/mobile")
public String register(User user){
try {
User userName = userRepository.findUserByUsername(user.getUsername());
if (userName != null){
return "用户名已存在";
}
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
user.setPassword("{bcrypt}"+encoder.encode(user.getPassword()));
List<Role> roles = new ArrayList<>();
Role role = userRoleRepository.findByName("ROLE_admin");
roles.add(role);
user.setRoles(roles);
userRepository.save(user);
}catch (Exception e){
return "出现了异常";
}
return "成果";
}
}

1.4 处理登录

查看代码

 package com.intehel.jwt.service;

import com.intehel.jwt.domain.User;
import com.intehel.jwt.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username)throws UsernameNotFoundException {
User user = userRepository.findUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
return user;
}
}

测试多方式注册和登录

1.测试注册功能

  这里使用测试工具Postman提交POST注册请求

  

  数据库插入信息如下

  

2. 测试登录功能

  浏览器输入https://localhost:8080/jwt自动跳转至登录界面,输入使用postman注册的账号即可

  以本博客对spring security的随笔,可实现使用token授权登录,这里不多做解释