SpringSecurity账户密码登录

引入依赖
  • 认证

①自定义登录接口

调用ProviderManager的方法进行认证 如果认证通过生成jwt

把用户信息存入redis中

②自定义UserDetailsService

在这个实现类中去查询数据库

校验:

①定义Jwt认证过滤器

获取token

解析token获取其中的userid

从redis中获取用户信息

存入SecurityContextHolder

 

说人话:

1.通过实现UserDetailService接口(自己实现LoadByUsername接口查到用户和用户对应的权限,封装为一个UserDetail(我们自己实现他的一个实现类LoginUser,重写其中的方法))

2.

注册BcryptPasswordEncoder();

由于使用AuthenticationManager(实现类ProviderManager)的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器;

重写WebSecurityConfigurerAdapter的configure方法;

注册jwt认证过滤器

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)//允许注解授权
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    //认证失败处理器
    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;
    //权限失败处理器
    @Autowired
    private AccessDeniedHandler accessDeniedHandler;
    //jwt认证过滤器
    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
                  //添加过滤器
                http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
         //配置异常处理器
        http.exceptionHandling()
            .authenticationEntryPoint(authenticationEntryPoint).
                accessDeniedHandler(accessDeniedHandler);
        
          //允许跨域
        http.cors();
    }
}

3.成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key

下面列出如何写一个登录认证

@RestController
public class LoginController {
​
    @Autowired
    private LoginServcie loginServcie;
​
    @PostMapping("/user/login")
    public ResponseResult login(@RequestBody User user){
        return loginServcie.login(user);
    }
}
@Service
//自己定义的loginService
public class LoginServiceImpl implements LoginServcie {
​
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    //自己封装了一层的redisTemplate
    private RedisCache redisCache;
​
    @Override
    public ResponseResult login(User user) {
        //传入账户密码,得到验证对象
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        //authenticationManager就是去调用我们刚才实现的UserDetailService去认证
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("用户名或密码错误");
        }
        //使用userid生成token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);
        //序列化存入redis(记得配置redis的JSON序列化)
        redisCache.setCacheObject("login:"+userId,loginUser);
        //把token响应给前端
        HashMap<String,String> map = new HashMap<>();
        map.put("token",jwt);
        return Result(200,,map);
    }
}
RedisConfig
package com.zhouy.blog.config;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import com.alibaba.fastjson.parser.ParserConfig;
import org.springframework.util.Assert;
import java.nio.charset.Charset;
​
/**
 * Redis使用FastJson序列化  
 * Warning fastJoson 序列化对象到redis再反序列化回来,对象要有无参构造函数不然报not auto Type
 */
public class FastJsonRedisSerializer<T> implements RedisSerializer<T>
{
​
    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
​
    private Class<T> clazz;
​
    static
    {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }
​
    public FastJsonRedisSerializer(Class<T> clazz)
    {
        super();
        this.clazz = clazz;
    }
​
    @Override
    public byte[] serialize(T t) throws SerializationException
    {
        if (t == null)
        {
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }
​
    @Override
    public T deserialize(byte[] bytes) throws SerializationException
    {
        if (bytes == null || bytes.length <= 0)
        {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);
​
        return JSON.parseObject(str, clazz);
    }
​
​
    protected JavaType getJavaType(Class<?> clazz)
    {
        return TypeFactory.defaultInstance().constructType(clazz);
    }
}

 

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
 @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
​
        FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
​
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
​
        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);
​
        template.afterPropertiesSet();
        return template;
    }
}

4.jwt认证过滤器,取出header中的token,对token进行解析取出其中的userid。

使用userid去redis中获取对应的LoginUser对象。

然后封装Authentication对象存入SecurityContextHolder

@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
​
    @Autowired
    private RedisCache redisCache;
​
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, ServletException, IOException {
        //获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //放行
            filterChain.doFilter(request, response);
            return;
        }
        //解析token
        Long userid = null;
        try {
            Map<String, Object> stringObjectMap = JWTUtils.checkToken(token);
            Object userid1 = stringObjectMap.get("userId");
            //object ->long
            userid= Long.valueOf(String.valueOf(userid1));
            log.info(String.valueOf(userid));
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        //从redis中获取用户信息
​
        String redisKey = "login:" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }
        //将认证对象存入SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser.getUsername(),loginUser.getPassword(),loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

5.推出登录

我们只需要定义一个登陆接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可。

完整的LoginServiceImpl

@Service
public class LoginServiceImpl implements LoginService {
​
    @Autowired
    private AuthenticationManager authenticationManager;
​
    @Autowired
    private RedisCache redisCache;
​
​
    @Override
    public Result login(User user) {
​
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
        //需要一个Authentication对象验证,这里传入他的实现类
        //使用authenticationManager.authenticate()传入要认证的对象信息,认证成功
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if (Objects.isNull(authenticate)) {
            throw new RuntimeException("用户名密码错误");
        }
        //从验证结果 authenticate 取得用户id 使用userid生成token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        //能取到id?
        Long userId = loginUser.getUser().getId();
        System.out.println(userId);
        String token = JWTUtils.createToken(userId);
        //用户信息存入redis,这里应该以token为key?
        redisCache.setCacheObject("login:" + userId, loginUser,10, TimeUnit.MINUTES);
        // 响应token给前端
        return Result.success(token);
    }
​
    @Override
    public Result logout() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userid = loginUser.getUser().getId();
        redisCache.deleteObject("login"+userid);
        return Result.success("退出成功");
    }
}

我的疑惑,登录成功之后要不要之间把已经认证成功的认证对象放入SecurityContextHolder

  • 授权

在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。

所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。

然后设置我们的资源所需要的权限即可。

LoginUser完成了对用户信息和权限的组合封装

@Data
@NoArgsConstructor //fastJson反序列化需要
public class LoginUser implements UserDetails {
    private User user;
    private List<String> permissions;
    //存储SpringSecurity所需要的权限信息的集合
    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities;
​
    public LoginUser(User user, List<String> list) {
        this.user=user;
        this.permissions=list;
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(authorities!=null){
            return authorities;
        }
        //把Permissions中的字符类型权限转换成GrantedAuthority对象存入authories中,stream流
        authorities = permissions.stream().
                map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return authorities;
    }
​
    @Override
    public String getPassword() {
        return user.getPassword();
    } //passwordEncoder在这里运行(内部)
​
    @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;
    }
}
  • 异常处理

    认证失败或者是授权失败的情况下 在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

    如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理

所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。

//授权失败
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "权限不足");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response,json);
​
    }
}
//认证失败
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response,json);
    }
}
​
CorsConfig

①springBoot允许跨域

@Configuration
public class CorsConfig implements WebMvcConfigurer {
​
    @Override
    public void addCorsMappings(CorsRegistry registry) {
      // 设置允许跨域的路径
        registry.addMapping("/**")
                // 设置允许跨域请求的域名
                .allowedOriginPatterns("*")
                // 是否允许cookie
                .allowCredentials(true)
                // 设置允许的请求方式
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                // 设置允许的header属性
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(3600);
    }
}

②springSecurity允许跨域 在SecurityConfig 的configure方法中开启http:cors();

 

posted @ 2022-10-15 19:38  zhouylove  阅读(94)  评论(0)    收藏  举报