Project1_07_过渡2_SpringBoot整合Security实现用户登录验证&用户身份认证&用户权限获取

一、项目环境

  1. 后端技术栈:SpringBoot, SpringSecurity, jwt
  2. 后端软体:IntelliJ IDEA2020, jdk1.8
  3. 数据库:mySql

二、文章主题

  1. 内容概述:为将Project07与SpringSecurity/Shiro+jwt整合,主要参考了MarkerHub_前后端分离后台管理系统进行改写,本文是关于该视频P30~46内容的学习整理,前端内容在传送门
  2. 项目源码:shoppingProject01_pub : version7.1

三、用户登录验证&用户身份认证 实现过程

待实现的SpringSecurity后台逻辑如图1所示。

在这里插入图片描述

图1 本项目中的Security流程图

Step1:SpringBoot整合Security,jwt

  1. porm.xml中引入相关jar包,引入后访问任意实现了的后端接口,会跳转到如图2所示的界面。用户名默认是"user";密码在SpringBoot终端有显示。
		<!-- springboot security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

在这里插入图片描述

图2 Security的默认路径拦截页面

Step2:整合验证码过滤器的 用户首次登录认证功能实现

1)首次登录认证:用户名、密码、验证码,完成登录。
2)图片验证码校验逻辑如图3所示。前后端不分离项目中图片验证码保存在session中;本项目为前后端分离,故将图片验证码保存在redis中。
在这里插入图片描述

图3 图片验证码校验逻辑

前后端不分离项目验证码图片可直接返回给前端url,前后端分离项目生成图片验证码的controller代码如下:
    // 生成图片验证码
    @GetMapping("/getCaptcha")
    public Result getCaptcha() throws IOException {
        String imageKey = UUID.randomUUID().toString();  // 图片验证码在redis中的key
        String imageCode = VerifyCodeUtils.generateVerifyCode(4);
        // 为了测试
        imageKey = "aaaaa";
        imageCode = "11111";
        // 将图片转为base64
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        VerifyCodeUtils.outputImage(120, 30, byteArrayOutputStream, imageCode);
        String str = "data:image/png;base64,";
        String base64Img = str + Base64Utils.encodeToString(byteArrayOutputStream.toByteArray());
        redisUtil.hset(Const.CAPTCHA_KEY,imageKey,imageCode,300);  // redis存储了图片验证码信息
        return Result.succ(
                MapUtil.builder()
                        .put("imageKey",imageKey)
                        .put("captchaImg",base64Img)
                        .build()
        );
    }

3)解决前后端分离的跨域问题:
1)):写跨域配置类
2)): 写Security配置类

package com.salieri.config;

import com.salieri.security.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    LoginFailureHandler loginFailureHandler;
    @Autowired
    LoginSuccessHandler loginSuccessHandler;
    @Autowired
    CaptchaFilter captchaFilter;
    @Bean
    JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
        return jwtAuthenticationFilter;
    }
    @Autowired
    JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    @Autowired
    JwtAccessDeniedHandler jwtAccessDeniedHandler;
    @Bean   // 构造用户密码的加密形式,这里用于Security判断登录时解析密码成明文
    BCryptPasswordEncoder bCryptPasswordEncoder() { 
        return new BCryptPasswordEncoder();
    }
    @Autowired
    UserDetailServiceImpl userDetailService; // 用于依据用户名在数据库中查询用户密码(Security)

	// Security放行的白名单
    private static final String[] URL_WHITELIST = {
            "/eb/login",
            "/eb/logout",
            "/user/getCaptcha",
            "/user/mailReg",
            "/user/activationMail",
            "/user/nickReg",
            "/favicon.ico"
    };

    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()    // 解决了跨域问题
        // 登录配置
        .formLogin()
                .successHandler(loginSuccessHandler)   // 见图1,这是认证成功处理器
                .failureHandler(loginFailureHandler)   // 认证失败处理器
                // 退出
//                .and()
//                .logout()
//                .logoutSuccessHandler(jwtLogoutSuccessHandler)
        // 禁用session
        .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        // 配置拦截规则
        .and()
                .authorizeRequests()
                .antMatchers(URL_WHITELIST).permitAll()   // 白名单链接要放行
                .anyRequest().authenticated()
      // 异常处理器
        .and()
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint) // 见图1,这是认证异常处理器
                .accessDeniedHandler(jwtAccessDeniedHandler)           // 权限异常处理器
        // 配置自定义的过滤器
        .and()
                .addFilter(jwtAuthenticationFilter())   // 见图1,这是BasicAuthentication
                // 在用户名、密码校验过滤器前要加入图片验证码过滤器
                .addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
        ;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService);
    }

}

4)写LoginFailureHandler(认证失败处理器)、LoginSuccessHandler(认证成功处理器)、CatpchaFilter(图片验证码处理器)、JwtLogoutSuccessHandler(退出处理器)。

Step3:基于jwt的用户二次token认证功能实现

1)二次token认证:请求头携带jwt进行身份认证。
2)写JwtUtils并在application.properties中写相应的参数。
3) 在LoginSuccessHandler中将jwt放置到返回给前端响应的请求头

// 注入到spring中
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    JwtUtils jwtUtils;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = response.getOutputStream();

        // 生成jwt,并放置到请求头中
        String jwt = jwtUtils.generateToken(authentication.getName());
        response.setHeader(jwtUtils.getHeader(),jwt);

        Result result = Result.succ("");
        outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));

        outputStream.flush();
        outputStream.close();
    }
}

4)写BasicAuthicationFilter(身份认证过滤器,当用户发起的不是登录请求时,对请求头中的jwt进行解析,完成自动登录功能)

// 用户访问登录以外页面时的jwt认证
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

    @Autowired
    JwtUtils jwtUtils;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String jwt = request.getHeader(jwtUtils.getHeader());
        if (StrUtil.isBlankOrUndefined(jwt)) {
            // 过滤器链直接往后面走,这是为了放行白名单
            chain.doFilter(request,response);
            return;
        }
        // 解析jwt
        Claims claim = jwtUtils.getClaimByToken(jwt);
        if ( claim == null ) { // jwtUtils解析不合法就会返回空
            throw new JwtException("token异常");
        }
        // 判断jwt是否过期
        if (jwtUtils.isTokenExpired(claim)) {
            throw new JwtException("token已过期");
        }
        // 通过主体拿到用户名称
        String username = claim.getSubject();
        // 获取用户的权限信息
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username,null,null);
        // 设置认证主体以完成自动登录
        SecurityContextHolder.getContext().setAuthentication(token);
        // 让过滤器链继续往后走
        chain.doFilter(request,response);
    }
}

5)写异常处理Filter(JwtAuthenticationEntryPotin认证失败异常;JwtAccessDeniedHandler权限不足异常)
6)实现用户登录时查库、匹配库中用户数据
1)) 自定义AccountUser(作为匹配成功的用户信息返回),写UserDetailServiceImpl
注:在Security配置类要让Security知道数据库中用户密码的加密形式

package com.salieri.security;

import com.salieri.pojo.User;
import com.salieri.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;

// 看,这个service是security中的
@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.getByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户名或密码不正确");
        }
        // AccountUser是自定义的
        return new AccountUser(user.getId(),user.getUsername(),user.getPassword(),getUserAuthority(user.getId()));
    }

    // 获取用户权限信息(角色权限、网站操作权限)
    public List<GrantedAuthority> getUserAuthority(int userId) {
        return null;
    }
}

2)) 在Security配置类中配置重写后的UserDetailService。

四、用户权限获取 实现过程

1) 初步设计的项数据库信息userinfo,roleinfo如图4、5所示:
在这里插入图片描述

图4 userinfo

在这里插入图片描述

图5 roleinfo

Question: 我们在哪里赋予用户权限?

Place1:用户登录,调用UserDetailService.loadUserByUsername()方法时,可以返回用户的权限信息。
Place2:接口调用进行身份认证过滤器时JWTAuthenticationFilter,需要返回用户权限信息。
2)实现过程
1)) 在userDetailserviceImpl文件中相关代码如下:

    @Autowired
    UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.getUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户名或密码不正确");
        }
        return new AccountUser(user.getId(),user.getUsername(),user.getPassword(),getUserAuthority(user.getUsername()));  // 授权时传递用户角色
    }

    // 获取用户权限信息(角色权限、网站操作权限)
    public List<GrantedAuthority> getUserAuthority(String username) {

        // 角色(ROLE_normal)、网站操作权限( sys:item:list )
        String authority = userService.getUserAuthorityInfo(username);  // ROLE_normal, sys:item:list, ...

        return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
    }

2)) 在UserServiceImpl中相关代码如下:

    @Override
    public String getUserAuthorityInfo(String username) {
        // ROLE_normal, sys:item:list, ...
        String authority = "";

        // 获取到角色,加前缀
        User user = userDAO.getUserByUsername(username);
        authority = "ROLE_" + user.getRole();
        // 获取网站操作权限相关的编码
        String permsList = roleDAO.getPermsByRolename(user.getRole());
        authority = authority.concat(",").concat(permsList);
        return authority;
    }

3)) 在JWTAuthenticationFilter中相关代码如下:

// 获取用户的权限信息
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username,null,userDetailService.getUserAuthority(user.getUsername()));
  1. 给用户的权限信息加缓存:
    1)) 在UserServiceImpl中相关代码如下:
    @Override
    public String getUserAuthorityInfo(String username) {
        // ROLE_normal, sys:item:list, ...
        String authority = "";

        if ( redisUtil.hasKey("GrantedAuthority:"+username) ) {
            authority = (String)redisUtil.get("GrantedAuthority:"+username);
        } else {
            // 获取到角色,加前缀
            User user = userDAO.getUserByUsername(username);
            authority = "ROLE_" + user.getRole();
            // 获取网站操作权限相关的编码
            String permsList = roleDAO.getPermsByRolename(user.getRole());
            authority = authority.concat(",").concat(permsList);
            // 对当前用户相关权限字符串进行缓存
            redisUtil.set("GrantedAuthority:"+username,authority,60*60);
        }

        return authority;
    }

这引发了一个问题,什么时候清除缓存?
设计一个清除缓存的接口:

   @Override
    public void clearUserAuthorityInfo(String username) {
        redisUtil.del("GrantedAuthority:"+username);
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值