Spring Security和JWT结合
先引入pom.xml,主要看dependency部分:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
<relativePath/> <!– lookup parent from repository –>
</parent>
<groupId>com.maxshu</groupId>
<artifactId>test_SpringSecurity</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>test_SpringSecurity</name>
<description>test_SpringSecurity</description>
<properties>
<java.version>17</java.version>
<kotlin.version>1.8.22</kotlin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</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>
<!– Spring Security –>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!– Hutool JWT –>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-jwt</artifactId>
<version>5.8.22</version>
</dependency>
<!– GSon, Json –>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies>
下面是配置文件application.yml:
server:
address: 0.0.0.0
port: 80
logging:
level:
org.springframework.security.web.FilterChainProxy: trace
org.springframework.security.web.access.ExceptionTranslationFilter: trace
org.springframework.security: debug
spring:
application:
name: test_SpringSecurity
profiles:
active: dev
main.banner-mode: ‘off’
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: db
password: db_password
url: jdbc:mysql://127.0.0.1:3306/test?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
jpa:
database: MYSQL
show-sql: true
generate-ddl: true
database-platform: org.hibernate.dialect.MySQLDialect
hibernate:
ddl-auto: update
naming:
implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy # Springboot 3.x用
# physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy # Springboot 2.x用
properties:
hibernate:
format_sql: true
use_sql_comments: true
要建立一个配置类:
@Configuration
@EnableWebSecurity
//@EnableMethodSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //controller启用注解机制的安全,PreAuthorize才会有效
public class WebSecurityConfig {
private static final Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);
@Autowired
private MyAccessDeniedHandler accessDeniedHandler;
@Autowired
private MyUnauthorizedHandler unauthorizedHandler;
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
return new JwtAuthenticationTokenFilter();
}
@Bean
public JwtAuthenticationProvider jwtAuthenticationProvider(){
return new JwtAuthenticationProvider();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public WebSecurityCustomizer ignoringCustomizer() {
//不需要鉴权的访问放这里,在filter之前执行:
return (web) -> web.ignoring()
//如果这些路径没有在Controller中配置,用这些路径来访问的话也会鉴权失败,而且要跟Controller中严格匹配,包括访问时尾部不要”/”。
//允许对于网站静态资源的无授权访问
.requestMatchers(HttpMethod.GET,”/”,”/*.html”,”/*.css”,”/*.js”,”/static/**”)
//对登录注册允许匿名访问
.requestMatchers(“/user/login”,”/user/register”,”/user/logout”)
//跨域请求会先进行一次options请求
.requestMatchers(HttpMethod.OPTIONS)
//测试时全部运行访问.permitAll();
.requestMatchers(“/test/**”);
}
@Bean
public SecurityFilterChain filterChain2(HttpSecurity httpSecurity) throws Exception {
httpSecurity
//由于使用的是JWT,这里不需要csrf防护
.csrf((csrf)->csrf.disable())
//已经经过鉴权filter的访问,再走http过滤:
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated());
//基于token,所以不需要session
httpSecurity.sessionManagement((sm)-> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// 禁用缓存
httpSecurity.headers((header)->header.cacheControl((control)->control.disable()));
//使用自定义provider
httpSecurity.authenticationProvider(jwtAuthenticationProvider());
//添加JWT filter
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//添加自定义未授权和未登录结果返回
httpSecurity.exceptionHandling((handle)->handle
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(unauthorizedHandler));
return httpSecurity.build();
}
}
下面是JwtAuthenticationProvider类:
//登录的时候用来鉴权
public class JwtAuthenticationProvider implements AuthenticationProvider {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationProvider.class);
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = String.valueOf(authentication.getPrincipal());
String password = String.valueOf(authentication.getCredentials());
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if(passwordEncoder.matches(password,userDetails.getPassword())){
if(userDetails instanceof UserDetailsImpl) {
((UserDetailsImpl)userDetails).setIsLogin(true);
}
return new UsernamePasswordAuthenticationToken(username, password, userDetails.getAuthorities());
}
throw new BadCredentialsException("Password is error!");
}
@Override
public boolean supports(Class<?> authentication) {
// return UsernamePasswordAuthenticationToken.class.equals(authentication);
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
下面是JwtAuthenticationTokenFilter类:
//不能定义为Component、Service等。
//所有访问都会来过滤。
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
private final static String AUTH_HEADER = "Authorization";
private final static String AUTH_HEADER_TYPE = "Bearer";
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationProvider jwtAuthenticationProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// get token from header: Authorization: Bearer <token>
String authHeader = request.getHeader(AUTH_HEADER);
if (Objects.isNull(authHeader) || !authHeader.startsWith(AUTH_HEADER_TYPE)){
//不做鉴权处理,所以会直接返回鉴权失败
filterChain.doFilter(request,response);
return;
}
String authToken = authHeader.split(" ")[1];
logger.debug("authToken:{}" , authToken);
//verify token,token的提供在AuthController的登录时给的。
if (!JWTUtil.verify(authToken, MyConstant.JWT_SIGNER)) {
logger.info("invalid JWT token.");
//不做鉴权处理,所以会直接返回鉴权失败
filterChain.doFilter(request,response);
return;
}
final String userName = (String) JWTUtil.parseToken(authToken).getPayload("username");
UserDetails userDetails = userDetailsService.loadUserByUsername(userName);
if(request.getRequestURI().equals("/user/logout")){ //退出登录
if(userDetails instanceof UserDetailsImpl) {
((UserDetailsImpl)userDetails).setIsLogin(false);
}
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
Message message = new Message<>(true, "退出登录成功");
Gson gson = new Gson();
response.getWriter().println(gson.toJson(message));
response.getWriter().flush();
response.getWriter().close();
return; //不再过滤了。
}
if(userDetails instanceof UserDetailsImpl){
if(!((UserDetailsImpl)userDetails).getIsLogin()){ //没有登录,即使带了AUTH字段也不行
//不做鉴权处理,所以会直接返回鉴权失败
filterChain.doFilter(request,response);
return;
}
}
//带了token,且已经登录的处理。
// 注意,这里使用的是3个参数的构造方法,此构造方法将认证状态设置为true
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//将认证过了凭证保存到security的上下文中以便于在程序中使用
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
下面是MyAccessDeniedHandler类:
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
private static final Logger logger = LoggerFactory.getLogger(MyAccessDeniedHandler.class);
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//logger.error("access error: "+accessDeniedException.getMessage(), accessDeniedException);
logger.error("access error: "+accessDeniedException.getMessage());
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
Message message = new Message<>(false, "禁止访问");
Gson gson = new Gson();
response.getWriter().println(gson.toJson(message));
response.getWriter().flush();
response.getWriter().close();
}
}
下面是MyUnauthorizedHandler类:
@Component
public class MyUnauthorizedHandler implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(MyUnauthorizedHandler.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//logger.error("Unauthorized error: "+authException.getMessage(), authException);
logger.error("Unauthorized error: "+authException.getMessage());
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
Message message = new Message<>(false, "认证失败");
Gson gson = new Gson();
response.getWriter().println(gson.toJson(message));
response.getWriter().flush();
response.getWriter().close();
}
}
下面是MyConstant类:
public class MyConstant {
private static final String JWT_SIGN_KEY = "yunbu_key_de$xxSia4R2#@dffDE";
public static final HMacJWTSigner JWT_SIGNER = new HMacJWTSigner(
AlgorithmUtil.getAlgorithm("HMD5"), JWT_SIGN_KEY.getBytes(StandardCharsets.UTF_8));
}
本地调试需要,增加一个CORS_Filter类来允许跨域请求:
@Component
public class CORS_Filter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest reqs = (HttpServletRequest) req;
String curOrigin = reqs.getHeader("Origin");
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", curOrigin == null ? "true" : curOrigin);
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "POST, PUT, PATCH, GET, OPTIONS, DELETE, HEAD");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Origin, X-Api-Key, Aaccess-Control-Allow-Origin, Authority, Authorization, Content-Type, Accept, Version-Info, X-Requested-With");
// response.setContentType("application/json;charset=UTF-8");
chain.doFilter(req, res);
}
}
下面为Role和User相关的几个类:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
private RoleType roleType;
}
public enum RoleType {
ADMIN("admin"),
USER("user");
private String roleName;
RoleType(String roleName) {
this.roleName = roleName;
}
public String getRoleName() {
return roleName;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String userName;
private String password;
private static Boolean isLogin = false; //static是临时测试为了保持登录状态,后续挪到数据库后就不能用static了。
public static void setIsLogin(Boolean isLogin){
User.isLogin = isLogin;
}
public static Boolean getIsLogin(){
return User.isLogin;
}
private List<Role> roles;
}
@RequiredArgsConstructor
public class UserDetailsImpl implements UserDetails {
private final User user;
public void setIsLogin(Boolean isLogin){
User.setIsLogin(isLogin);
}
public Boolean getIsLogin(){
return User.getIsLogin();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles()
.stream()
.map(role -> new SimpleGrantedAuthority(role.getRoleType().getRoleName()))
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = getUserByName(username);
if(user == null){
throw new UsernameNotFoundException("User isn't exist!");
}
return new UserDetailsImpl(user);
}
public User getUserByName(String userName) {
if (!"007".equals(userName)) {
return null; //找不到user
}
// List<Role> roles = List.of(new Role(RoleType.ADMIN), new Role(RoleType.USER));
// List<Role> roles = List.of( new Role(RoleType.USER));
List<Role> roles = List.of( new Role(RoleType.ADMIN));
return new User(userName, passwordEncoder.encode("123456"), roles);
}
}
几个和前端打交道的DTO类:
@Data
public class Message<T> {
private Boolean success;
private String msg;
private Page page = null;
private List<T> dataList = null;
public Message(){
;
}
public Message(Boolean success, String msg){
setSuccessAndMsg(success, msg);
}
public void setSuccessAndMsg(Boolean success, String msg){
this.success = success;
this.msg = msg;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Page{
private Integer num;
private Integer size;
private Integer total;
private Long totalRec;
}
}
@Data
public class SignInReq {
private String username;
private String password;
}
@Data
@AllArgsConstructor
public class JWTAuthToken {
String jwtToken;
}
最后两个controller类,第一个处理登陆注册:
@RestController
@RequestMapping("/user")
public class AuthController {
private static final Logger logger = LoggerFactory.getLogger(AuthController.class);
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping ("/login")
public Message<JWTAuthToken> postLogin(@RequestBody SignInReq req) {
logger.info("postLogin, req: {}", req);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(req.getUsername(), req.getPassword());
authenticationManager.authenticate(authenticationToken); //在config里设置了之后,这里会调用JwtAuthenticationProvider的鉴权
//上一步没有抛出异常说明登录成功,我们向用户颁发jwt令牌,检验在JwtAuthenticationTokenFilter里面。
String token = JWT.create()
.setPayload("username", req.getUsername())
.setSigner(MyConstant.JWT_SIGNER)
.sign();
//登录成功,返回JWT格式的token给客户端后续再请求时从Header的
JWTAuthToken jwtAuthToken = new JWTAuthToken(token);
Message<JWTAuthToken> message = new Message<>(true, "");
message.setDataList(new ArrayList<JWTAuthToken>(Arrays.asList(jwtAuthToken)));
return message;
}
@GetMapping ("/login")
public String getLogin() {
logger.info("getLogin");
return "getLogin";
}
@PostMapping ("/register")
public Message<String> postRegister(@RequestBody SignInReq req) {
logger.info("postRegister, req: {}", req);
//存入数据库。
Message<String> message = new Message<>(true, "");
message.setDataList(new ArrayList<String>(Arrays.asList("注册成功")));
return message;
}
}
第二个用来处理业务:
@RestController
@RequestMapping("/aaa")
public class TestController {
private static final Logger logger = LoggerFactory.getLogger(TestController.class);
@RequestMapping("/aaa")
public Message<String> aaa() {
logger.info("aaa");
Message<String> message = new Message<>(true, "");
message.setDataList(new ArrayList<String>(Arrays.asList("aaa")));
return message;
}
@PreAuthorize("hasAuthority('admin')") //需要以什么角色来访问
@GetMapping("/bbb")
public Message<String> bbb(){
Message<String> message = new Message<>(true, "");
message.setDataList(new ArrayList<String>(Arrays.asList("bbb")));
return message;
}
@PreAuthorize("hasAuthority('user')")
@GetMapping("/ccc")
public Message<String> ccc(){
Message<String> message = new Message<>(true, "");
message.setDataList(new ArrayList<String>(Arrays.asList("ccc")));
return message;
}
@PreAuthorize("hasAuthority('user') || hasAuthority('admin')")
@GetMapping("/ddd")
public Message<String> ddd(){
Message<String> message = new Message<>(true, "");
message.setDataList(new ArrayList<String>(Arrays.asList("ddd")));
return message;
}
}
我们使用curl来模拟注册、登陆、访问、退出登录等操作:
//注册(存数据库时才有用,现在临时测试固定了用户没意义): curl -X POST -H 'Content-Type:application/json' -d '{"username":"007","password":"123456"}' 'http://127.0.0.1/user/register'
//模拟登录: curl -X POST -H 'Content-Type:application/json' -d '{"username":"007","password":"123456"}' 'http://127.0.0.1/user/login'
//会打印出返回的 token,记住该token。
//模拟登录后的访问:curl -X GET -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJITUQ1In0.eyJ1c2VybmFtZSI6IjAwNyJ9.nj2Wp_-CCQGZ44-8uomApw' 'http://127.0.0.1/aaa/aaa'
//这里路径/aaa/aaa、/aaa/bbb、/aaa/ccc、/aaa/ddd的处理参考:TestController
//模拟退出登录: curl -X GET -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJITUQ1In0.eyJ1c2VybmFtZSI6IjAwNyJ9.nj2Wp_-CCQGZ44-8uomApw' 'http://127.0.0.1/user/logout'