前置基础请参考:SpringSecurity入门-CSDN博客
配置:
pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.5</version>
</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-security</artifactId>
</dependency>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.23</version>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!--mysql + mybatis-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
</dependencies>
application.yaml
server:
port: 8080
spring:
data:
redis:
host: 你的redisurl
port: 6379
password: 你的密码
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: 你的数据库url
username: 你的用户名
password: 你的密码
#配置security的默认账号和密码
# security:
# user:
# name: admin
# password: admin
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
一、自定义登录
1、实现UserDetailsService
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<User>().eq(User::getName, username);
User user = userMapper.selectOne(queryWrapper);
if(null == user){
throw new RuntimeException("用户名或密码错误");
}
LoginUser loginUser = new LoginUser(user);
return loginUser;
}
}
我这里使用的是根据用户名查询,一般情况下是使用userid,这个时候可能会报异常,因为此时的密码校验方式是以明文的形式,所以我们需要将加密器注入到
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder PasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
2、自定义统一结果返回对象
@Data
@AllArgsConstructor
public class ResponseResult {
private Integer Code; //状态码
private String message; //信息
private Object data; //数据
}
3、自定义登录接口
@RestController
@RequestMapping("/user")
public class HelloController {
@Autowired
private LoginService loginService;
@PostMapping("/login")
public ResponseResult login(@RequestBody User user){
ResponseResult result = loginService.login(user);
return result;
}
@RequestMapping("/success")
public ResponseResult success(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
ResponseResult responseResult = new ResponseResult(200, "success", loginUser);
return responseResult;
}
@RequestMapping("/fail")
public String fail(){
return "fail";
}
}
二、使用jwt令牌
1、自定义登录方式及其实现
在这里我们需要提取出用户信息,将用户信息存入redis缓存之中,并且使用用户id生成jwt令牌,将jwt令牌封装进返回体中,调用其他接口时就可以从jwt令牌中提取出用户id,然后去redis中提取用户信息。
public interface LoginService {
public ResponseResult login(User user);
}
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Override
public ResponseResult login(User user) {
//获取认证方法进行用户认证Authentication
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getName(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//认证未通过
if(ObjectUtils.isNull()){
throw new RuntimeException("用户名或者密码错误");
}
//提取用户信息
LoginUser loginuser = (LoginUser) authenticate.getPrincipal();
String id = loginuser.getUser().getId().toString();
//生成jwt
String jwt = JwtUtils.createJWT(id);
//将用户信息存入redis
String jsonString = JSON.toJSONString(loginuser);
redisTemplate.opsForValue().set("userId:" + id,jsonString,3, TimeUnit.MINUTES);
//将jwt存入token
HashMap<String, String> map = new HashMap<>();
map.put("token",jwt);
ResponseResult responseResult = new ResponseResult(200, "success", map);
//返回
return responseResult;
}
}
jwtutils:
public class JwtUtils {
//有效期为
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "sangeng";
public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @param ttlMillis token超时时间
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis=JwtUtils.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("sg") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
}
/**
* 创建token
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
return builder.compact();
}
public static void main(String[] args) throws Exception {
// String jwt = createJWT("2123");
// Claims claims = parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyOTY2ZGE3NGYyZGM0ZDAxOGU1OWYwNjBkYmZkMjZhMSIsInN1YiI6IjIiLCJpc3MiOiJzZyIsImlhdCI6MTYzOTk2MjU1MCwiZXhwIjoxNjM5OTY2MTUwfQ.NluqZnyJ0gHz-2wBIari2r3XpPp06UMn4JS2sWHILs0");
// String subject = claims.getSubject();
// System.out.println(subject);
// System.out.println(claims);
//String jwt = createJWT("1234");
Claims claims = parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIwNjc3ZTE4NDJhMTg0MDNjYmE5MDM3ZjUwYzUwYWFlMyIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTcxNDkxMzE0NiwiZXhwIjoxNzE0OTE2NzQ2fQ.7IsMNeWewvNqoZB5Wys0cagCqv4m014DZNrNGZRjB_E");
String subject = claims.getSubject();
System.out.println("subject = " + subject);
}
/**
* 生成加密后的秘钥 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtils.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
2、webmvc和security配置
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
}
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder PasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用basic明文验证
//.httpBasic().disable()
// 前后端分离架构不需要csrf保护
.csrf().disable()
// 禁用默认登录页
//.formLogin().disable()
// 禁用默认登出页
//.logout().disable()
// 设置异常的EntryPoint,如果不设置,默认使用Http403ForbiddenEntryPoint
//.exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(invalidAuthenticationEntryPoint))
// 前后端分离是无状态的,不需要session了,直接禁用。
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
// 允许所有OPTIONS请求
//.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 允许直接访问授权登录接口
.requestMatchers(HttpMethod.POST, "/user/login").permitAll()
// 允许 SpringMVC 的默认错误地址匿名访问
//.requestMatchers("/error").permitAll()
// 其他所有接口必须有Authority信息,Authority在登录成功后的UserDetailsImpl对象中默认设置“ROLE_USER”
//.requestMatchers("/**").hasAnyAuthority("ROLE_USER")
// 允许任意请求被已登录用户访问,不检查Authority
.anyRequest().authenticated());
//.authenticationProvider(authenticationProvider())
// 加我们自定义的过滤器,替代UsernamePasswordAuthenticationFilter
//.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
3、用户信息存入SecurityContextHolder
为了简化操作流程,方便我们在编写其他接口时可以很方便的提取出用户信息,我们可以自定义过滤器,提取出用户信息,并且存入SecurityContextHodler里
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//从请求头中提取出用户id
String jwt = request.getHeader("token");
//id不存在
if(!StringUtils.hasText(jwt)){
filterChain.doFilter(request,response);
return;
}
//解析jwt
String id = null;
try {
Claims claims = JwtUtils.parseJWT(jwt);
id = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
filterChain.doFilter(request,response);
}
//使用用户id从redis里面提取出用户信息
String jsonstring = redisTemplate.opsForValue().get("userId:" + id);
LoginUser loginUser = JSON.parseObject(jsonstring, LoginUser.class);
//将用户信息存入securityContextHolder
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request,response);
}
}
需要添加我们自定义的过滤器,使其生效,添加.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用basic明文验证
//.httpBasic().disable()
// 前后端分离架构不需要csrf保护
.csrf().disable()
// 禁用默认登录页
//.formLogin().disable()
// 禁用默认登出页
//.logout().disable()
// 设置异常的EntryPoint,如果不设置,默认使用Http403ForbiddenEntryPoint
//.exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(invalidAuthenticationEntryPoint))
// 前后端分离是无状态的,不需要session了,直接禁用。
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
// 允许所有OPTIONS请求
//.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 允许直接访问授权登录接口
.requestMatchers(HttpMethod.POST, "/user/login").permitAll()
// 允许 SpringMVC 的默认错误地址匿名访问
//.requestMatchers("/error").permitAll()
// 其他所有接口必须有Authority信息,Authority在登录成功后的UserDetailsImpl对象中默认设置“ROLE_USER”
//.requestMatchers("/**").hasAnyAuthority("ROLE_USER")
// 允许任意请求被已登录用户访问,不检查Authority
.anyRequest().authenticated())
//.authenticationProvider(authenticationProvider())
// 加我们自定义的过滤器,替代UsernamePasswordAuthenticationFilter
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
三、测试
因为登录需要使用post请求,所以我们使用postman进行测试
需求:
1、登录
localhost:8080/user/login 登陆成功返回信息里携带jwt令牌
2、测试jwt令牌
localhost:8080/user/success携带jwt令牌提取用户信息