前言
回顾:
【Spring Security】springboot + mybatis-plus + mysql 从数据库读取用户信息验证登录
【Spring Security】springboot + mybatis-plus + mysql 密码加密存储下的数据库用户验证登录
本节实现token验证登录功能,仅仅实现功能能用,不完善。
配置
application.properties
#spring.datasource.name=mysql
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&serverTimezone=Asia/Shanghai&characterEncoding=utf8&autoReconnect=true&useSSL=false&allowMultiQueries=true
spring.datasource.username=xxx
spring.datasource.password=xxx
spring.security.jwt.secret=e9948PG02lURjvhjotDGQ6ksRdz3920MEfdy0q6HIszaxNNXw5D1yGq7l3zVWVfUbPBSA56JMqawy7Mt2vPDx5AveuOHHpT0uZB
# token expire time limit 20000ms = 20s, 14400000ms = 14400s = 240m = 4h
spring.security.jwt.expMillis=20000
UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.sample.dao.entity.UserPattern">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="org.sample.dao.entity.UserPattern">
<id column="id" property="id" />
<result column="username" property="username" />
<result column="passwd" property="passwd" />
<result column="enabled" property="enabled" />
<result column="account_non_expired" property="account_non_expired" />
<result column="account_non_locked" property="account_non_locked" />
<result column="credentials_non_expired" property="credentials_non_expired" />
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, username, passwd, enabled, account_non_expired, account_non_locked, credentials_non_expired
</sql>
</mapper>
pom.xml
<?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>
<groupId>org.example</groupId>
<artifactId>spring-security-learning</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>spring-security-02</module>
<module>spring-security-01</module>
<module>spring-security-03</module>
<module>spring-security-04</module>
<module>spring-security-05</module>
<module>spring-security-06</module>
</modules>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web-services</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>
<!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
</dependency>
<!-- jdbc启动器 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
</dependencies>
</project>
编码
common.utils.JWTUtils
package org.sample.common.utils;
import com.alibaba.fastjson.JSON;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.MacSigner;
import java.util.Map;
public class JWTUtils {
/**
* 创建JWT
* @param secret
* @param claims 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
* @return
*/
public static String getAccessToken(String secret, Map<String, Object> claims){
// 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
MacSigner rsaSigner = new MacSigner(secret);
Jwt jwt = JwtHelper.encode(JSON.toJSONString(claims), rsaSigner);
return jwt.getEncoded();
}
public static Map<String,Object> parseToken(String token){
Jwt jwt = JwtHelper.decode(token);
return JSON.parseObject(jwt.getClaims());
}
/**
* 根据传入的token过期时间判断token是否已过期
* @param expiresIn
* @return true-已过期,false-没有过期
*/
public static boolean isExpiresIn(long expiresIn){
long now=System.currentTimeMillis();
return now>expiresIn;
}
}
config.filter.JwtAuthenticationTokenFilter
package org.sample.config.filters;
import lombok.extern.slf4j.Slf4j;
import org.sample.common.utils.JWTUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private String tokenHead = "Bearer ";
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String tokenValue = request.getHeader(HttpHeaders.AUTHORIZATION);
if (!StringUtils.hasText(tokenValue)){
filterChain.doFilter(request, response);
return;
}
String token = tokenValue.substring(tokenHead.length());
Map<String, Object> parseJWT = JWTUtils.parseToken(token);
if (JWTUtils.isExpiresIn((long) parseJWT.get("expiresIn"))){
// token 已经过期
SecurityContextHolder.getContext().setAuthentication(null);
filterChain.doFilter(request, response);
return;
}
String username = (String) parseJWT.get("username");
if (StringUtils.hasText(username) && SecurityContextHolder.getContext().getAuthentication() == null){
// 正常用户
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if(userDetails != null && userDetails.isEnabled()){
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 设置用户登录状态
log.info("authenticated user {}, setting security context", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}
handler
UsernamePasswordAuthenticationEntryPoint
package org.sample.handler;
import com.alibaba.fastjson.JSON;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* 异常策略
* @author
* @des:
*/
@Component
public class UsernamePasswordAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
response.setHeader("WWW-Authenticate", "Bearer");
response.addHeader("Access-Control-Allow-Origin", "*");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
PrintWriter out = response.getWriter();
Map<String,Object> data = new HashMap<>();
data.put("path", request.getRequestURI());
data.put("time", LocalDateTime.now().toString());
data.put("errCode", HttpStatus.UNAUTHORIZED.value());
data.put("errMsg", HttpStatus.UNAUTHORIZED.getReasonPhrase());
out.write(JSON.toJSONString(data));
out.flush();
out.close();
}
}
UsernamePasswordAuthenticationFailureHandler.Java
package org.sample.handler;
import com.alibaba.fastjson.JSON;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 失败策略
* @author
*/
@Component
public class UsernamePasswordAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(JSON.toJSONString(exception));
out.flush();
out.close();
}
}
UsernamePasswordAuthenticationSuccessHandler.java
package org.sample.handler;
import com.alibaba.fastjson.JSON;
import org.sample.common.utils.JWTUtils;
import org.sample.dao.entity.UserPattern;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
public class UsernamePasswordAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Value("${spring.security.jwt.secret: huangxuanheng@163.com}")
private String secret;
@Value("${spring.security.jwt.expMillis: 7200000}")
private long expMillis;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Object principal = authentication.getPrincipal();
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
User user = (User) principal;
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("auth"));
// 生成token
Map<String, Object> claims = new HashMap<>();
claims.put("username", user.getUsername());
claims.put("authorities", user.getAuthorities());
claims.put("enabled", user.isEnabled());
claims.put("expiresIn", (System.currentTimeMillis() + expMillis));
String token = JWTUtils.getAccessToken(secret, claims);
Map<String,Object>result = new HashMap<>();
result.put("accessToken", token);
out.write(JSON.toJSONString(result));
out.flush();
out.close();
}
}
UsernamePasswordLogoutSuccessHandler.java
package org.sample.handler;
import com.alibaba.fastjson.JSON;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author
*/
@Component
public class UsernamePasswordLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Object principal = authentication.getPrincipal();
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(JSON.toJSONString(principal));
out.flush();
out.close();
}
}
config.SecurityConfig
package org.sample.config;
import com.alibaba.fastjson.JSON;
import org.sample.config.filters.JwtAuthenticationTokenFilter;
import org.sample.service.UserService;
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.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import java.io.PrintWriter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* PasswordEncoder
* @return PasswordEncoder
*/
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 方法二
* 实现configure(AuthenticationManagerBuilder auth)配置方法
* 配置文件中的默认用户和默认密码注释掉了
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
/**
* 方法一
* auth.userDetailsService(userDetailsService())
*/
// @Bean
// public UserDetailsService userDetailsService(){
// return new UserDetailsService() {
// // username是用户登录时填的用户名
// public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// UserPattern user = new UserPattern();
// user.setUsername("mike");
// user.setPassword("123456");
// try {
// user = userService.loadByUserName(username);
// } catch (Exception e) {
// e.printStackTrace();
// }
//
// // 如果查不到用户名,这里可以抛出UsernameNotFoundException异常
// // 根据username查询权限,这里假设从任意位置查到权限是auth
// List<GrantedAuthority> authorities = new ArrayList<>();
// authorities.add(new SimpleGrantedAuthority("auth"));
// // User是系统自带的UserDetails实现类,4个状态其中一个为false就会抛异常
// return new User(user.getUsername(), user.getPassword(), true, true, true, true, authorities);
// }
// };
// }
/**
* 通过HttpSecurity 对象,获取到表单登录配置对象,修改对应的用户名和密码参数名称,即可完成自定义用户名和密码参数名称
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated()
.and()
.formLogin()
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.permitAll()
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.logout()
.logoutSuccessHandler(logoutSuccessHandler)
.and().csrf().disable()
;
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
public static void main(String[] args) {
}
}
controller.HelloController
package org.sample.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping("/sayHello")
public String sayHello(){
return "十年生死两茫茫,不思量,自难忘----苏轼,hello";
}
}
dao.entity.UserPattern
package org.sample.dao.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 测试用entity
*
* @author
* @since
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@TableName(value = "test.h_user")
public class UserPattern implements Serializable {
/**
* ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户名 userName
*/
@TableField(value = "username")
private String username;
/**
* 密码 password
*/
@TableField(value = "passwd")
private String password;
@TableField(value = "enabled")
private int enabled;
@TableField(value = "account_non_expired")
private int accountNonExpired;
@TableField(value = "account_non_locked")
private int accountNonLocked;
@TableField(value = "credentials_non_expired")
private int credentialsNonExpired;
}
dao.mapper.UserServiceMapper
package org.sample.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.sample.dao.entity.UserPattern;
/**
*
* @author
* @since
*/
public interface UserServiceMapper extends BaseMapper<UserPattern> {
}
service.UserService
package org.sample.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.sample.dao.entity.UserPattern;
/**
*
* @author
* @since
*/
public interface UserService extends IService<UserPattern> {
UserPattern loadByUserName(String username);
}
service.Impl.UserServiceImpl
package org.sample.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.sample.dao.entity.UserPattern;
import org.sample.dao.mapper.UserServiceMapper;
import org.sample.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserServiceImpl extends ServiceImpl<UserServiceMapper, UserPattern> implements UserService, UserDetailsService {
@Autowired
private UserServiceMapper userServiceMapper;
/**
* 方法一
* auth.userDetailsService(userDetailsService())
* @param username
* @return
*/
@Override
public UserPattern loadByUserName(String username) {
List<UserPattern> userPatterns = userServiceMapper.selectList(Wrappers.lambdaQuery(UserPattern.class)
.likeLeft(UserPattern::getUsername, username)
.likeRight(UserPattern::getUsername, username)
);
UserPattern userPattern = userPatterns.get(0);
return userPattern;
}
/**
* 方法二
* auth.userDetailsService(userDetailsService)
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<UserPattern> userPatterns = userServiceMapper.selectList(Wrappers.lambdaQuery(UserPattern.class)
.likeLeft(UserPattern::getUsername, username)
.likeRight(UserPattern::getUsername, username)
);
UserPattern userPattern = userPatterns.get(0);
boolean enabled = userPattern.getEnabled() == 1;
// 如果查不到用户名,这里可以抛出UsernameNotFoundException异常
// 根据username查询权限,这里假设从任意位置查到权限是auth
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("auth"));
return new User(userPattern.getUsername(), userPattern.getPassword(), enabled, true, true, true, authorities);
}
}
MainApplication
package org.sample;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("org.sample.dao.mapper")
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
文件结构
运行
运行MainApplication.java
postman登录:
复制得到的这个token:
写入sayHello接口的token中,访问接口:
再过20秒之后,token会自动失效,此时再访问:
显示未认证,说明token失效,需要重新登录获取token,成功!
postman使用时记得关掉这个设置,否则带不带token,都能够调用接口,因为postman自动记住token了:
|