前置基础请参考: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: 你的redisurlport: 6379password: 你的密码datasource:type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: 你的数据库urlusername: 你的用户名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;@Overridepublic 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 {@Beanpublic 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 {@Autowiredprivate 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 {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisTemplate<String,String> redisTemplate;@Overridepublic ResponseResult login(User user) {//获取认证方法进行用户认证AuthenticationUsernamePasswordAuthenticationToken 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();//生成jwtString jwt = JwtUtils.createJWT(id);//将用户信息存入redisString jsonString = JSON.toJSONString(loginuser);redisTemplate.opsForValue().set("userId:" + id,jsonString,3, TimeUnit.MINUTES);//将jwt存入tokenHashMap<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 {@Beanpublic PasswordEncoder PasswordEncoder(){return new BCryptPasswordEncoder();}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();}@Beanpublic 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 {@Autowiredprivate RedisTemplate<String,String> redisTemplate;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//从请求头中提取出用户idString jwt = request.getHeader("token");//id不存在if(!StringUtils.hasText(jwt)){filterChain.doFilter(request,response);return;}//解析jwtString 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);//将用户信息存入securityContextHolderUsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);filterChain.doFilter(request,response);}
}
需要添加我们自定义的过滤器,使其生效,添加.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Beanpublic 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令牌提取用户信息