springSecurity登录的全过程

登录

为了更好的表达整个程序流程,以下顺序全部按照我编码思路流程进行

登录思路

思路:

  1. 请求登录接口,进入方法
    进入一下接口,loginBody中是登录专用实体类,里面只有username和password。
	 /**
     * 登录方法
     *
     * @param loginBody 登录信息
     * @return 结果
     */
    @PostMapping("/login")
    public Res login(@RequestBody LoginBody loginBody) {
        System.out.println("登录"+loginBody);
        return sysUserService.login(loginBody);
    }
  1. 用户验证
    在登录方法中,首先进行登录验证。至于为什么会进入UserDetailsService,请查看authenticationManager.authenticate()调用UserDetailsServiceImpl.loadUserByUsername过程
//用户验证   该方法会去调用UserDetailsServiceImpl.loadUserByUsername
        Authentication authentication = authenticationManager
                .authenticate(new UsernamePasswordAuthenticationToken(loginBody.getUserName(), loginBody.getPassword()));

进入到UserDetailsService.loadUserByUsername()中,会返回UserDetails,UserDetails是用户的详细信息。
此时要实现UserDetailsService接口的loadUserByUsername()方法去做一些,原因在下面的代码注释中。

@Service
@Slf4j
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private SysUserMapper sysUserMapper;

    /**
     * 定义自己的登陆逻辑不走安全框架的默认
     * 备注:
     * security拥有自己的登录界面和登录逻辑
     * 为了使用自己的登录逻辑,创建一个UserDetailServiceImpl实现框架的UserDetailsService
     * authenticationManager.authenticate()会调用次方法
     * @param username
     * @return UserDetails 为了返回用户信息,创建了一个登录类LoginUser继承了UserDetails
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser user = sysUserMapper.selectOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getUserName, username));
        if (user==null) {
            log.info("登录用户:{} 不存在.", username);
            throw new RuntimeException("登录用户:" + username + " 不存在");
        } else if ("2".equals(user.getDelFlag())) {
            log.info("登录用户:{} 已被删除.", username);
            throw new RuntimeException("对不起,您的账号:" + username + " 已被删除");
        } else if ("2".equals(user.getStatus())) {
            log.info("登录用户:{} 已被停用.", username);
            throw new RuntimeException("对不起,您的账号:" + username + " 已停用");
        }
        //这是权限,应该从数据库中获取的,为了方便使用,直接在这里写死了,后续使用中因该联合用户信息、角色、菜单查询
        Set<String> permissions=new HashSet<>();
        permissions.add("admin");
        //把获取到的登录用户信息添加到LoginUser返回
        LoginUser loginUser=new LoginUser(user.getUserId(),user.getDeptId(),user, permissions);
        System.out.println("UserDetailServiceImpl自己定义的登陆逻辑返回结果"+loginUser);
        //次方法中返回值是UserDetails,但最后返回的是LoginUser,是因为LoginUser实现了UserDetails类
        //注意这里的LoginUser不能是登录的实体类。因为会造成username的null指针异常。还有loginUser中重写的方法给默认值改成true;
        return loginUser;
    }
}
  1. 获取用户信息

	   //获取当前用户信息
        LoginUser loginUserInfo = (LoginUser) authentication.getPrincipal();
        System.out.println("登陆接口中用户验证结果" + loginUserInfo);
        // 生成token
        return Res.ok(createToken(loginUserInfo));

从上一步验证中获取到登录用户信息。次方法可以封装成一个SecurityUtils安全服务工具类,用于获取用户的各种信息。

  1. 生成token信息返回给前端
    生成token的流程是,先生成一个随机数,把这个随机数作为key,登录用户信息作为value,存入到redis中,接着把token通过jwt进行加密,把加密过后的随机数返回给前端。在前端进行其他请求的时候,前端在请求头中把加密过后的token传给后台,后台通过jwt解析,把redis中存的key解析出来,再去redis获取用户信息。
 public String createToken(LoginUser loginUserInfo) {
        //获取一个随机数token
        String token = UUID.randomUUID().toString();
        loginUserInfo.setToken(token);
        //将token作为key,loginUserInfo作为value,存入redis
        redisTemplate.opsForValue().set("login_user_key"+token, loginUserInfo);
        Map<String, Object> claims = new HashMap<>();
        claims.put("uuid", token);
        //将生成的随机数进行加密返回给前端,后续过程中,前端会通过传这个个加密之后的token1,然后解密出token随机数,然后去redis中取出用户信息
        String token1 = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, "myruoyiitem").compact();
        System.out.println("生成token随机数" + token);
        System.out.println("对token随机数进行加密" + token1);
        return token1;
    }

总结

整个流程就是:进入接口—>进行验证—>获取用户信息—>创建token—>存入redis—>返回

注意点

登录所用的实体和实现UserDetails的实体不能是一个,具体的空指针我忘记在哪里出现了,反正遇到这个问题了。

添加认证

认证的目的

目的就是让用户请求的时候,携带加密之后的随机数(token),去验证用户的信息

认证过程

  1. 创建认证过滤器JwtAuthenticationTokenFilter,记得实现OncePerRequestFilter。下面获取token的过程就是获取请求头token,然后解密出token随机数,然后去redis中取出用户信息
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    TokenUtil tokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        System.out.println("对于在SecurityConfig中没有放行的都进入到这个接口");
        //获取token
        LoginUser user = tokenUtil.getLoginUser(request);
        if (user != null) {
            //验证令牌有效期,相差不足20分钟,自动刷新缓存,预防三十分钟之后自动失效
            //tokenUtil.verifyToken(user);
            //TODO 获取权限信息封装到Authentication中
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        //放行
        filterChain.doFilter(request, response);
    }
}
  1. 创建完成之后,把JwtAuthenticationTokenFilter加入到SecurityConfig过滤器链中
/**
 * SecurityConfig设置
 * @author lenovo
 * @date 2022/6/20
 * @EnableGlobalMethodSecurity注解prePostEnabled开启注解功能
 *
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    private UserDetailsService userDetailsService;
    /**
     * token认证过滤器
     */
    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    /**
     * 认证失败处理类
     */
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;
    /**
     * 授权失败处理类
     */
    @Autowired
    private AccessDeniedHandlerImpl accessDeniedHandler;

    // 配置用于放行login页面
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭csrf
        http.csrf().disable()
                // 认证失败、授权失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).accessDeniedHandler(accessDeniedHandler).and()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login", "/register").anonymous()
                .antMatchers("/login", "/register").permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        //把token校验过滤器添加到过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

    }

    /**
     * 将AuthenticationManager作为对象注入容器
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 把PasswordEncoder注入bean
     * 加密方法
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

备注
1. 为什么设置.antMatchers(“/login”,“/register”).anonymous()?
此时,用户登录的时候就会首先进入JwtAuthenticationTokenFilter,因为没有token,直接放行,但是在SecurityConfig中,并不会放行,请求会返回状态码401,没有认证成功。但是如果设置.antMatchers(“/login”,“/register”).anonymous()之后,请求会放行登录注册接口。

处理认证失败的请求

上面说到,请求认证失败之后会返回给前端一个状态码401,此时,我们需要做一下异常处理。来处理一下这个401状态,使返回的状态使是200,code=401

/**
 * 认证失败处理类
 *
 * @author ruoyi
 */
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {
    private static final long serialVersionUID = -8970718410437077606L;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
            throws IOException {
        int code = 401;
        String msg = "请求访问:{" + request.getRequestURI() + "},认证失败,无法访问系统资源";
        try {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(JSON.toJSONString(Res.failed(code,msg)));
        } catch (IOException e1) {
            e1.printStackTrace();
        }
    }
}

处理之后并不是就这样能生效,而是还需要再SecurityConfig配置这个异常处理

http.csrf().disable()
                // 认证失败、授权失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)

这样才能生效。

添加授权

一般我们都是使用注解的方式去进行授权。

在请求带 @PreAuthorize(“@ss.hasPermi(‘admin’)”)的接口中。都进行授权。也需要一个授权的服务。注意注解ss是服务中名称。

 /**
     * 测试token
     *
     * @return 结果
     */
    @GetMapping("/testToken")
    @PreAuthorize("@ss.hasPermi('admin')")
    public Res testToken() {
        System.out.println("测试token");
        return Res.ok("测试通过");
    }

自定义权限实现

/**
 * 自定义权限实现,ss取自SpringSecurity首字母
 *
 * @author lenovo
 * @date 2022/6/21
 */
@Service("ss")
public class PermissionService {
    /**
     * 所有权限标识
     */
    private static final String ALL_PERMISSION = "*:*:*";

    /**
     * 管理员角色权限标识
     */
    private static final String SUPER_ADMIN = "admin";

    private static final String ROLE_DELIMETER = ",";

    private static final String PERMISSION_DELIMETER = ",";

    /**
     * 验证用户是否具备某权限
     *
     * @param permission 权限字符串
     * @return 用户是否具备某权限
     */
    public boolean hasPermi(String permission) {
        System.out.println("拥用这个权限才能通过"+permission);
        if (StringUtils.isEmpty(permission)) {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        System.out.println("当前用户信息"+loginUser);
        if (loginUser==null|| CollectionUtils.isEmpty(loginUser.getPermissions())) {
            return false;
        }
        return hasPermissions(loginUser.getPermissions(), permission);
    }
    /**
     * 判断是否包含权限
     *
     * @param permissions 权限列表
     * @param permission 权限字符串
     * @return 用户是否具备某权限
     */
    private boolean hasPermissions(Set<String> permissions, String permission)
    {
        return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
    }
}

重点:配置完成之后,也别注意,此时注解还不能使用,需要在SecurityConfig开启注解。再SecurityConfig类加上注解开启
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)

处理授权失败异常

/**
 * 授权失败处理类
 * @author lenovo
 * @date 2022/6/21
 */
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        int code = 403;
        String msg = "请求访问:{" + request.getRequestURI() + "},失败,权限不足";
        try {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(JSON.toJSONString(Res.failed(code,msg)));
        } catch (IOException e1) {
            e1.printStackTrace();
        }
    }
}

这样,如果授权失败,请求返回的是一个403状态,根据认证失败的处理过程,同理授权页需要再SecurityConfig配置授权

http.csrf().disable()
                // 认证失败、授权失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).accessDeniedHandler(accessDeniedHandler).and()

注册注意的点

在使用注册的时候,加密方式一定要和登录相同,
数据库添加用户信息时,密码一定要生成BCryptPasswordEncoder密码再添加。

  • 4
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

XuDream

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值