赞
踩
代码传送门
链接:https://pan.baidu.com/s/19ii5ffaweX0p52ptRGTD-w
提取码:m6j7
SpringSecurity https://spring.io/projects/spring-security#overviewopen in new window
Spring家族当中,一个安全管理框架。
Shiro也是一个安全框架,提供了很多安全功能。Shiro比较老,旧的项目当中,可能还在使用。上手还挺简单。
在新项目当中,一线互联网大型项目,都是使用SpringSecurity 。
一般的web项目当中,总会有登陆和鉴权的需求。但是大家一定要区分开。
所以说,安全框架SpringSecurity 当中,必定会有认证和鉴权的两大核心功能。
快速构建
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.2</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <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> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> </dependencies>
创建controller
@RestController
public class TestController {
@GetMapping("test")
public String test() {
return "123Test";
}
}
启动 测试 访问 :http://localhost:8099/test
<!-- 引入security起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
访问 : http://localhost:8099/test
Security 自带的登陆页面
可以输入自带默认用户名 user 和 密码(控制台)
Using generated security password: b6092291-ce28-4c5c-afc9-cb2e8c5fde06
就能访问到数据了
http://localhost:8099/logout
总而言之,自己的一些特定需求,都没有实现。
springsecurity 就是通过一些过滤器、拦截器,实现登陆鉴权的流程的。
springsecurity就是一个过滤器链,内置了关于springsecurity的过滤器。
我们可以找到当前boot项目中的,所有有关security的过滤器链。
登陆: 1自定义登录接口
调用prodivermanager auth方法
登陆成功生成jwt
存入redis
2自定义userdetailsmanager实现类
从数据库中获取系统用户
访问资源:自定义认证过滤器
获取token
从token中获取userid
从redis中通过userid获取用户信息
存SecurityContextHolder
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。无状态。
好处:不需要服务器端 存session。
特点:可以看到,但是不能篡改,因为第三部分用了秘钥。
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。 abcd.abcd.abcd
头部(Header)
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
{"typ":"JWT","alg":"HS256"}
在头部指明了签名算法是HS256算法。 我们进行BASE64编码[https://base64.us/
载荷(playload)
载荷就是存放有效信息的地方。
定义一个payload:
{"phone":"1234567890","login_user_key":"1"}
签证(signature)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header
payload
secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
hs256("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Iml0bGlscyIsImFkbWluIjp0cnVlLCJhZ2UiOjE4fQ==",secret)
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
JTdCJTIydHlwJTIyJTNBJTIySldUJTIyJTJDJTIyYWxnJTIyJTNBJTIySFMyNTYlMjIlN0Q=.JTdCJTIyc3ViJTIyJTNBJTIyMTIzNDU2Nzg5MCUyMiUyQyUyMm5hbWUlMjIlM0ElMjJqYWNrJTIyJTJDJTIyYWRtaW4lMjIlM0F0cnVlJTdE.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
官方文档:
https://github.com/jwtk/jjwtopen in new window
public String generateToken(String phone) {
Calendar instance = Calendar.getInstance();
// 设置过期时间
instance.add(Calendar.SECOND, 1000);
return Jwts.builder()
.setSubject(phone)//主题
.setIssuedAt(new Date(System.currentTimeMillis()))//签发日期
.setExpiration(instance.getTime())// 设置过期时间
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact();
}
我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。
public LoginUser getLoginUser(HttpServletRequest request) {
String token = request.getHeader(header);
if(Objects.isNull(token) || ObjectUtils.isEmpty(token)) {
return null;
}
Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
// 解析对应的权限以及用户信息
String username = claims.get("sub").toString();
LoginUser user = redisCache.getCacheObject(CacheConstants.USER_INFO_KEY + username);
System.out.println(user);
return user;
}
JWT工具类代码
@Component @Slf4j public class JwtUtilService { /** * 注入header值 */ @Value("${token.header}") private String header; /** * 注入secret密钥 */ @Value("${token.secret}") private String SECRET; public String generateToken(String phone) { Calendar instance = Calendar.getInstance(); // 设置过期时间 instance.add(Calendar.SECOND, 1000); return Jwts.builder() .setSubject(phone)//主题 .setIssuedAt(new Date(System.currentTimeMillis()))//签发日期 .setExpiration(instance.getTime())// 设置过期时间 .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public String getLoginUser(HttpServletRequest request) { String token = request.getHeader(header); if(Objects.isNull(token) || ObjectUtils.isEmpty(token)) { return null; } Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); // 解析对应的权限以及用户信息 String username = claims.get("sub").toString(); return username; } /** * 检查token是否过期 * * @param token token * @return boolean */ public boolean isExpiration(String token) { Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); return claims.getExpiration().before(new Date()); } public boolean validateJwtToken(String authToken) { try { Jwts.parser().setSigningKey(SECRET).parseClaimsJws(authToken); return true; } catch (SignatureException e) { log.error("Invalid JWT signature: {}", e.getMessage()); } catch (MalformedJwtException e) { log.error("Invalid JWT token: {}", e.getMessage()); } catch (ExpiredJwtException e) { log.error("JWT token is expired: {}", e.getMessage()); } catch (UnsupportedJwtException e) { log.error("JWT token is unsupported: {}", e.getMessage()); } catch (IllegalArgumentException e) { log.error("JWT claims string is empty: {}", e.getMessage()); } return false; } }
利用Spring注入机制
server:
port: 8099
# token配置
token:
# 令牌自定义标识
header: Authorization
# 令牌密钥
secret: Yx7GcP3UY194v8U.fLyhBiFZFxKOagQZt1baEhKlTfMW
# 令牌有效期(默认30分钟)
expireTime: 30
LoginUser类
@Data
public class LoginUser {
//账号
private String userName;
//密码
private String password;
//验证码
private Integer code;
}
注意:设置签名key必须和生成时一致。
①添加依赖
<!--redis依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--fastjson依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.33</version> </dependency> <!--jwt依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <!--JAXB API是java EE 的API,因此在java SE 9.0 中不再包含这个 Jar 包。--> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency>
② 添加Redis相关配置
@Component public class RedisCache { @Autowired public RedisTemplate redisTemplate; /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 */ public <T> void setCacheObject(final String key, final T value) { redisTemplate.opsForValue().set(key, value); } /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 * @param timeout 时间 * @param timeUnit 时间颗粒度 */ public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { redisTemplate.opsForValue().set(key, value, timeout, timeUnit); } /** * 设置有效时间 * * @param key Redis键 * @param timeout 超时时间 * @return true=设置成功;false=设置失败 */ public boolean expire(final String key, final long timeout) { return expire(key, timeout, TimeUnit.SECONDS); } /** * 设置有效时间 * * @param key Redis键 * @param timeout 超时时间 * @param unit 时间单位 * @return true=设置成功;false=设置失败 */ public boolean expire(final String key, final long timeout, final TimeUnit unit) { return redisTemplate.expire(key, timeout, unit); } /** * 获得缓存的基本对象。 * * @param key 缓存键值 * @return 缓存键值对应的数据 */ public <T> T getCacheObject(final String key) { ValueOperations<String, T> operation = redisTemplate.opsForValue(); return operation.get(key); } /** * 删除单个对象 * * @param key */ public boolean deleteObject(final String key) { return redisTemplate.delete(key); } /** * 删除集合对象 * * @param collection 多个对象 * @return */ public long deleteObject(final Collection collection) { return redisTemplate.delete(collection); } /** * 缓存List数据 * * @param key 缓存的键值 * @param dataList 待缓存的List数据 * @return 缓存的对象 */ public <T> long setCacheList(final String key, final List<T> dataList) { Long count = redisTemplate.opsForList().rightPushAll(key, dataList); return count == null ? 0 : count; } /** * 获得缓存的list对象 * * @param key 缓存的键值 * @return 缓存键值对应的数据 */ public <T> List<T> getCacheList(final String key) { return redisTemplate.opsForList().range(key, 0, -1); } /** * 缓存Set * * @param key 缓存键值 * @param dataSet 缓存的数据 * @return 缓存数据的对象 */ public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) { BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key); Iterator<T> it = dataSet.iterator(); while (it.hasNext()) { setOperation.add(it.next()); } return setOperation; } /** * 获得缓存的set * * @param key * @return */ public <T> Set<T> getCacheSet(final String key) { return redisTemplate.opsForSet().members(key); } /** * 缓存Map * * @param key * @param dataMap */ public <T> void setCacheMap(final String key, final Map<String, T> dataMap) { if (dataMap != null) { redisTemplate.opsForHash().putAll(key, dataMap); } } /** * 获得缓存的Map * * @param key * @return */ public <T> Map<String, T> getCacheMap(final String key) { return redisTemplate.opsForHash().entries(key); } /** * 往Hash中存入数据 * * @param key Redis键 * @param hKey Hash键 * @param value 值 */ public <T> void setCacheMapValue(final String key, final String hKey, final T value) { redisTemplate.opsForHash().put(key, hKey, value); } /** * 获取Hash中的数据 * * @param key Redis键 * @param hKey Hash键 * @return Hash中的对象 */ public <T> T getCacheMapValue(final String key, final String hKey) { HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash(); return opsForHash.get(key, hKey); } /** * 删除Hash中的数据 * * @param key * @param hkey */ public void delCacheMapValue(final String key, final String hkey) { HashOperations hashOperations = redisTemplate.opsForHash(); hashOperations.delete(key, hkey); } /** * 获取多个Hash中的数据 * * @param key Redis键 * @param hKeys Hash键集合 * @return Hash对象集合 */ public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) { return redisTemplate.opsForHash().multiGet(key, hKeys); } /** * 获得缓存的基本对象列表 * * @param pattern 字符串前缀 * @return 对象列表 */ public Collection<String> keys(final String pattern) { return redisTemplate.keys(pattern); } }
@Configuration public class RedisConfig { /** * RedisTemplate可以接收任意Object作为值写入Redis,只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化,得到的一串很长的值 * 缺点:可读性查、浪费存储空间 */ @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){ // 1.创建 redisTemplate RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); // 2.设置连接工厂 redisTemplate.setConnectionFactory(redisConnectionFactory); // 3.设置序列化工具 GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); // key 和 hashkey 采用 String 序列化 redisTemplate.setKeySerializer(RedisSerializer.string()); redisTemplate.setKeySerializer(RedisSerializer.string()); // value 和 hashvalue 采用 json 序列化 redisTemplate.setValueSerializer(jsonRedisSerializer); redisTemplate.setHashValueSerializer(jsonRedisSerializer); return redisTemplate; }; }
③ 响应类
package com.qf.common; import lombok.Data; import lombok.ToString; import lombok.experimental.Accessors; /** * @auther: sin * @Date: 2023/6/23 - 06 - 23 - 14:13 * @Description: com.qf.common.pojo * @version: 1.0 */ @Data // 设置链式数据 @ToString @Accessors(chain = true) public class ResponseResult<T> { /** * 状态码 */ private int code; /** * 返回信息 */ private String message; /** * 返回数据 */ private T data; /** * 自定义返回成功数据 * @param data * @return * @param <T> */ public static <T> ResponseResult success(T data) { return new ResponseResult().setCode(200).setMessage("操作成功").setData(data); } /** * 自定义返回失败数据 * @param data * @return * @param <T> */ public static <T> ResponseResult fail(String message, T data) { return new ResponseResult().setCode(400).setMessage(message).setData(data); } }
**重要!**实现真实从数据库获取系统用户信息
创建UserDetailsService实现类,重写其中的方法。用户名从数据库中查询用户信息。
public class UserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //根据用户名去数据库查询用户信息 SysUser user = new SysUser(); //如果查询不到数据就通过抛出异常来给出提示 if(Objects.isNull(user)){ throw new RuntimeException("用户名错误"); } //TODO 根据用户查询权限信息 添加到LoginUser中 //封装成UserDetails对象返回 return new MyUserDetails(user); } }
1 实际项目中我们不会把密码明文存储在数据库中
2 默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。
我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。
我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。
SecurityConfig配置
由于spring boot3.0废弃了extends WebSecurityConfigurerAdapter的方式,所以这里采用添加@Bean新方式
@Configuration
public class SecurityConfig{
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
密码加密:
1 12345 md5 --> asdfasdfasdfasdfa 默认密码12345
2 12345 md5(12345|itlils)---->uiowertupouert 加盐
3 BCryptPasswordEncoder 自动加盐
过时问题:
首先,过时也能用,如果看着不爽,可以使用如下方法。
以前我们自定义类继承自 WebSecurityConfigurerAdapter 来配置我们的 Spring Security,我们主要是配置两个东西:
前者主要是配置 Spring Security 中的过滤器链,后者则主要是配置一些路径放行规则。
现在在 WebSecurityConfigurerAdapter 的注释中,人家已经把意思说的很明白了:
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig { /** * 认证失败处理类 */ @Resource private AuthenticationEntryPointImpl unauthorizedHandler; @Resource private AccessDeniedHandlerImpl accessDeniedHandler; /** * 登出处理 */ @Resource private LogoutSuccessHandlerImpl logoutSuccessHandler; /** * */ @Resource private AuthenticationConfiguration authenticationConfiguration; /** * token拦截器 */ @Resource JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; /** * 强散列哈希加密实现 */ @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http //关闭csrf .csrf().disable() // 认证失败处理类 .exceptionHandling() .authenticationEntryPoint(unauthorizedHandler) .and() //不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 对于登录接口 允许匿名访问 .requestMatchers("/login").anonymous() // 无论是否登录都可以访问 .requestMatchers("/captchaImage").permitAll() // 除上面外的所有请求全部需要鉴权认证 .requestMatchers().authenticated(); //把token校验过滤器添加到过滤器链中 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 添加Logout filter http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 允许跨域 http.cors(); return http.build(); } @Bean public AuthenticationManager authenticationManager() throws Exception{ AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager(); return authenticationManager; } }
分析需求:
1 自定义一个controller登陆接口
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@RequestMapping("/login")
public ResponseResult Login(@RequestBody LoginUser user, HttpServletResponse response) {
return loginService.login(response, user.getUserName(), user.getPassword());
}
}
2 放行自定义登陆接口
// 对于登录接口 允许匿名访问
.requestMatchers("/login").anonymous()
3使用ProviderManager auth方法进行验证
4自己生成jwt给前端
5系统用户相关所有信息放入redis
1.获取token
2.解析token
3.获取userid
4.封装Authentication
5.存入SecurityContextHolder
@Service public class LoginServiceImpl implements LoginService { @Autowired AuthenticationManager authenticationManager; @Autowired JwtUtilService jwtUtil; @Autowired RedisCache redisCache; @Override public ResponseResult login(HttpServletResponse response, String username, String password) { // 1登录前置校验 // PreCheck.loginPreCheck(username, password); // 2验证码校验 // PreCheck.validateCaptcha(code, uuid, redisCache); // 3使用ProviderManager auth方法进行验证 UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password); // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken); // 校验失败了 if (Objects.isNull(authenticate)) { return new ResponseResult<>().setCode(HttpStatus.ERROR).setMessage("用户名或密码错误!"); } MyUserDetails userDetails = (MyUserDetails) (authenticate.getPrincipal()); // 4自己生成jwt token给前端 String token = jwtUtil.generateToken(username); // 5系统用户token放入redis //redisCache.setCacheObject(CacheConstants.LOGIN_TOKEN_KEY + username, token, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES); // 6系统用户信息放入redis //redisCache.setCacheObject(CacheConstants.USER_INFO_KEY + username, userDetails, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES); System.out.println(userDetails); //System.out.println(loginUser.getUser()); // 根据用户id获取所有的角色信息 // List<SysUserRole> roleList = sysUserRoleService.RoleIdList(loginUser.getUser().getUserId()); Map<String, Object> map = new HashMap(); map.put("token", token); map.put("userInfo", userDetails.getUser()); return new ResponseResult<>().setCode(HttpStatus.SUCCESS).setMessage(Constants.SUCCESS).setData(map); } }
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired RedisCache redisCache; @Autowired JwtUtilService jwtUtilService; // //@Autowired //SysUserService sysUserService; //@Autowired //UserDetailsServiceImpl userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //1解析token MyUserDetails userDetails = jwtUtilService.getLoginUser(request); if(!Objects.isNull(userDetails)) { //2封装Authentication UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, null); System.out.println(usernamePasswordAuthenticationToken); //5存入SecurityContextHolder SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); } //放行,让后面的过滤器执行 filterChain.doFilter(request, response); } }
测试
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。