- 一、SpringSecurity简介
- 1、创建SpringSecurity项目
- 2、测试依赖是否导入成功
- 二、重写认证流程
- 1、导入对应依赖
- 2、配置数据库连接
- 3、导入工具类
- 4、实现UserDetails 接口
- 5、实现UserDetailsService接口
- 6、将BCryptPasswordEncoder注入到容器
- 三、实现登录接口
- 1、将AuthenticationManager注入到容器
- 2、登录接口放行
- 3、定义LoginService
- 4、定义LoginController
- 四、token认证
- 1、定义jwt认证过滤器
- 2、配置jwt认证过滤器
- 五、退出登录
官方介绍:Spring Security,这是一种基于 Spring AOP 和 Servlet 过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。
1、创建SpringSecurity项目在以前创建springboot项目基础上加上security依赖:
2、测试依赖是否导入成功org.springframework.boot spring-boot-starter-security
随便写一个controller接口,发出请求后出现以下验证页面就说明依赖第一个security项目创建成功。只有先登录验证之后才能继续访问接口,默认用户名是:user,密码在日志里面有输出,复制过来就行。
2、配置数据库连接mysql mysql-connector-java runtime org.springframework.boot spring-boot-starter-data-redis 2.5.0 com.alibaba fastjson 1.2.33 io.jsonwebtoken jjwt 0.9.0 com.alibaba druid-spring-boot-starter 1.1.23 com.baomidou mybatis-plus-boot-starter 3.4.2
spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test02
username: root
password: 1234
redis:
host: localhost
port: 6379
# password: 1234
database: 0
cache:
redis:
time-to-live: 1800000
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:/mapper
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "cx";
public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
}
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=JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("cx") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
}
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 token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";
Claims claims = parseJWT(token);
System.out.println(claims);
}
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
- 重写RedisTemplate
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
@Autowired
public RedisTemplate redisTemplate;
public void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}
public void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
public boolean expire(final String key, final long timeout)
{
return expire(key, timeout, TimeUnit.SECONDS);
}
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
return redisTemplate.expire(key, timeout, unit);
}
public T getCacheObject(final String key)
{
ValueOperations operation = redisTemplate.opsForValue();
return operation.get(key);
}
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
public long deleteObject(final Collection collection)
{
return redisTemplate.delete(collection);
}
public long setCacheList(final String key, final List dataList)
{
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
public List getCacheList(final String key)
{
return redisTemplate.opsForList().range(key, 0, -1);
}
public BoundSetOperations setCacheSet(final String key, final Set dataSet)
{
BoundSetOperations setOperation = redisTemplate.boundSetOps(key);
Iterator it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
}
public Set getCacheSet(final String key)
{
return redisTemplate.opsForSet().members(key);
}
public void setCacheMap(final String key, final Map dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
public Map getCacheMap(final String key)
{
return redisTemplate.opsForHash().entries(key);
}
public void setCacheMapValue(final String key, final String hKey, final T value)
{
redisTemplate.opsForHash().put(key, hKey, value);
}
public T getCacheMapValue(final String key, final String hKey)
{
HashOperations opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
public void delCacheMapValue(final String key, final String hkey)
{
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.delete(key, hkey);
}
public List getMultiCacheMapValue(final String key, final Collection
4、实现UserDetails 接口
SpringSecurity初始验证登录信息时,是通过它自己默认的用户名以及日志中生成的密码验证的。但是我们在项目中肯定不是使用它的用户信息,我们需要验证自己数据库中存在的用户信息。UserDetails是一个返回结果接口,里面封装了认证成功之后的用户信息,我们需要将信息替换成自己数据库对用用户信息,就需要实行这个接口。将用户实体作为成员变量,再通过lombok就可以实现用户信息封装。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return null;
}
@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;
}
}
5、实现UserDetailsService接口
UserDetailsService验证登录用户是否存在,是否合法等信息的,SpringSecurity初始认证不是认证我们数据库用户信息,所有需要重写此接口。接口返回结果类型是UserDetails ,上一步我们已经重写里面的方法,返回的结果就是用户验证成功后的信息。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
// 查询用户信息
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName, userName);
User user = userMapper.selectOne(wrapper);
// 未查询到用户
if(Objects.isNull(user)){
throw new RuntimeException("用户名不存在");
}
// 查询成功返回用户信息
return new LoginUser(user);
}
}
6、将BCryptPasswordEncoder注入到容器
BCryptPasswordEncoder类是一个加密解密类,只有将它注入到容器,替换掉SpringSecurity默认的密码验证机制PasswordEncoder ,密码验证才不需要说明验证机制。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
三、实现登录接口
1、将AuthenticationManager注入到容器
AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录(还有这样的操作?没想到吧),所以说AuthenticationManager一般不直接认证,AuthenticationManager接口的常用实现类ProviderManager 内部会维护一个List列表,存放多种认证方式。也就是说,核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式:用户名+密码(UsernamePasswordAuthenticationToken),邮箱+密码,手机号码+密码登录则对应了三个AuthenticationProvider。
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
2、登录接口放行
在系统中并不是所有的接口都需要先认证后访问的,登录接口不需要验证就应该能够被访问,需要将不用验证的接口配置好。
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
}
3、定义LoginService
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public R login(User user) {
// AuthenticationManager用户名密码认证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 认证失败
if(Objects.isNull(authenticate)){
throw new RuntimeException("登录失败");
}
// 认证成功
// 使用用户id,生成JWT
LoginUser principal = (LoginUser) authenticate.getPrincipal();
Long id = principal.getUser().getId();
String jwt = JwtUtil.createJWT(id.toString());
Map map = new HashMap<>();
map.put("token",jwt);
// 将用户信息存入redis
redisCache.setCacheObject("login:"+ id, principal);
return R.ok(map);
}
}
4、定义LoginController
@RestController
@RequestMapping("/login")
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping
public R login(@RequestBody User user){
return loginService.login(user);
}
}
四、token认证
1、定义jwt认证过滤器
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = httpServletRequest.getHeader("token");
if (!StringUtils.hasText(token)) {
// 放行
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
// 解析token
String id;
try {
Claims claims = JwtUtil.parseJWT(token);
id = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
// redis获取用户信息
String redisKey = "userid:" + id;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
// 存入securityContextHolder
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
2、配置jwt认证过滤器
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
五、退出登录
@GetMapping("/logout")
public R logout(){
// 从securityContextHolder获取用户id
UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser principal = (LoginUser) authenticationToken.getPrincipal();
Long id = principal.getUser().getId();
// 删除redis里面用户信息
redisCache.deleteObject("login:"+id);
return R.ok("注销成功");
}
暑期编程PK赛
得CSDN机械键盘等精美礼品!


