SpringBoot integrated security (4) | (Security is based on JWT to achieve front-end separation and custom login)

SpringBoot integrated security (4) | (Security is based on JWT to achieve front-end separation and custom login)


Chapter
Chapter 1 link: SpringBoot integrated security (1) | (Security entry)
Chapter 2 link: SpringBoot integrated security (2) | (security custom configuration)
Chapter 3 link: SpringBoot integrated security (3) | (security Front-end and back-end separation login and response processing)
Chapter 4 link: SpringBoot integrated security (4) | (Security is based on JWT to achieve front-end separation and custom login)

foreword

In the previous chapter, we introduced springboot's security-based user configuration, permission configuration, and resource configuration. And we have rewritten the login form to log in, and the error reporting of authentication and authorization exceptions has been dealt with uniformly, but there are still some problems. The projects are all separated from the front and back ends. In this chapter, we will implement the JWT login based on the separation of the front and back ends.

This article is an expansion on the basis of the previous one. If you are not very clear about the project foundation, please check the previous article

1. Project dependencies

It mainly includes security dependencies and some tool dependencies

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.7</version>
        <relativePath/>
    </parent>
   <dependencies>
       <!--    springboot   start-->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <!--    springboot依赖    end-->

       <!--wagger2依赖start-->
       <dependency>
           <groupId>com.github.xiaoymin</groupId>
           <artifactId>knife4j-spring-ui</artifactId>
           <version>3.0.3</version>
       </dependency>
       <dependency>
           <groupId>io.springfox</groupId>
           <artifactId>springfox-swagger2</artifactId>
           <version>3.0.0</version>
       </dependency>

       <!--常用工具依赖start-->
       <dependency>
           <groupId>org.apache.commons</groupId>
           <artifactId>commons-lang3</artifactId>
           <version>3.12.0</version>
       </dependency>

       <dependency>
           <groupId>org.apache.commons</groupId>
           <artifactId>commons-collections4</artifactId>
           <version>4.1</version>
       </dependency>

       <dependency>
           <groupId>com.google.guava</groupId>
           <artifactId>guava</artifactId>
           <version>30.1.1-jre</version>
       </dependency>

       <!--fastjson引入-->
       <dependency>
           <groupId>com.alibaba</groupId>
           <artifactId>fastjson</artifactId>
           <version>1.2.49</version>
       </dependency>
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <optional>true</optional>
       </dependency>
       <dependency>
           <groupId>commons-codec</groupId>
           <artifactId>commons-codec</artifactId>
           <version>1.15</version>
       </dependency>

       <!--security引入-->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-security</artifactId>
       </dependency>

       <!--数据库引入引入-->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-data-jpa</artifactId>
       </dependency>
       <dependency>
           <groupId>mysql</groupId>
           <artifactId>mysql-connector-java</artifactId>
           <scope>runtime</scope>
       </dependency>
       <!--jwt引入-->
       <dependency>
           <groupId>io.jsonwebtoken</groupId>
           <artifactId>jjwt</artifactId>
           <version>0.9.1</version>
       </dependency>

       <dependency>
           <groupId>joda-time</groupId>
           <artifactId>joda-time</artifactId>
           <version>2.9.9</version>
       </dependency>
   </dependencies>
   

2. Custom response processing

Custom response processing is mainly to define the response format, which is convenient for front-end and back-end coordination

1. Define the response body ResponseHandle

@Data
public class ResponseHandle<T> {
    
    
    private String status;
    private String desc;
    private T data;

    // 成功 无参构成函数
    public static ResponseHandle SUCCESS(){
    
    
        ResponseHandle result = new ResponseHandle();
        result.setDesc("成功");
        result.setResultCode(HttpCode.SUCCESS);
        return result;
    }
    //成功 有返回数据构造函数
    public static ResponseHandle SUCCESS(Object data){
    
    
        ResponseHandle result = new ResponseHandle();
        result.setData(data);
        result.setResultCode(HttpCode.SUCCESS);
        return result;
    }

    /**
     * 失败,指定status、desc
     */
    public static ResponseHandle FAIL(String status, String desc) {
    
    
        ResponseHandle result = new ResponseHandle();
        result.setStatus(status);
        result.setDesc(desc);
        return result;
    }

    /**
     * 失败,指定ResultCode枚举
     */
    public static ResponseHandle FAIL(HttpCode resultCode) {
    
    
        ResponseHandle result = new ResponseHandle();
        result.setResultCode(resultCode);
        return result;
    }
    /**
     * 把ResultCode枚举转换为ResResult
     */
    private void setResultCode(HttpCode code) {
    
    
        this.status = code.code();
        this.desc = code.message();
    }
}

2. Define the response enumeration HttpCode

public enum HttpCode {
    
    

    // 成功状态码
    SUCCESS("00000", "成功"),
    UNKNOWN_ERROR("99999", "服务未知异常"),
    // 系统500错误
    SYSTEM_ERROR("10000", "系统异常,请稍后重试"),


    // 认证错误:20001-29999
    USER_NOAUTH("20000", "用户未登录"),
    TOKEN_ERROR("20001", "生成token失败"),
    LOGIN_ERROR("20002", "登录失败"),
    USER_LOCKED("20004", "账户已锁定"),
    USER_PASS_OUT("20005", "用户名或密码错误次数过多"),
    USER_NOTFIND_ERROR("20006", "没有找到用户"),
    USER_ERROR("20007", "用户名或密码不正确"),
    USER_CODE("20008", "验证码输入有误,请重新输入!"),
    USER_DISABLE("20009", "该账号也被禁用,请联系管理员!"),
    USER_INFOERROR("20010", "用户信息获取异常!"),
    USER_NOAUTHON("20011", "用户没有权限访问"),

    ;


    private String code;

    private String message;

    HttpCode(String code, String message) {
    
    
        this.code = code;
        this.message = message;
    }

    public String code() {
    
    
        return this.code;
    }

    public String message() {
    
    
        return this.message;
    }
}

Three, custom user login response class

Mainly when a user accesses resources, it is checked that the user is not logged in, and the format of the returned front-end information in the scenario where the user is logged in and has no access rights

1. Prompt that the user is not logged in

@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
    
    
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
    
    
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.setStatus(HttpServletResponse.SC_OK);
        ResponseHandle fail = ResponseHandle.FAIL(HttpCode.USER_NOAUTHON);
        response.getWriter().write(JSONObject.toJSONString(fail));
    }
}

2. The user does not have access rights to process

@Component
public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint {
    
    

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    
    
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.setStatus(HttpServletResponse.SC_OK);
        ResponseHandle fail = ResponseHandle.FAIL(HttpCode.USER_NOAUTH);
        response.getWriter().write(JSONObject.toJSONString(fail));
    }
}

Fourth, custom login implementation

1. Introduction to JwtAuthencationTokenFilter

The BasicAuthenticationFilter filter has the same effect as the OncePerRequestFilter filter. It is a filter for security to implement user login authentication. Change the filter to pick up when the user visits. We can inherit this class, and then rewrite the doFilterInternal method to implement custom token interception , so that every time the interface accesses the resource, the token will be received to check whether it is valid and expired.

2. Realization of custom username and password login interface

Here, users can customize their own implementation logic, such as user password encryption, return parameters, etc.

1. Login interface

@Api(tags = {
    
    "登录相关接口"})
@RestController
@RequestMapping("/oak")
public class LoginCtrl {
    
    

    @Autowired
    private LoginService loginService;

    @ApiOperation(value = "用户登录接口", notes = "登录")
    @PostMapping("/login")
    public ResponseHandle login(@RequestBody User user, HttpServletRequest request) {
    
    
        return loginService.login(user, request);
    }
}

2. Log in to implement the server

@Service
public class LoginServiceImpl implements LoginService {
    
    

    @Autowired
    private UserServiceImpl userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;


    @Override
    public ResponseHandle login(User user, HttpServletRequest request) {
    
    
        String username = user.getUsername();
        String password = user.getPassword();

//        String code = user.getCode();
//        // 验证码
//        String captcha = (String) request.getSession().getAttribute("captcha");
//        // 判断验证码
//        if ("".equals(code) || !captcha.equalsIgnoreCase(code)) {
    
    
//            return ResponseHandle.FAIL(HttpCode.USER_CODE);
//        }
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (userDetails.isEnabled()) {
    
    
            if (null == userDetails || !passwordEncoder.matches(password, userDetails.getPassword())) {
    
    
                return ResponseHandle.FAIL(HttpCode.USER_ERROR);
            }
            // 更新security登录用户对象
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
//            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            //将authenticationToken放入spring security全局中
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            // 创建一个token
            String token = JwtTokenUtils.createToken(username, "", true);
            Map<String, String> tokenMap = new HashMap<>();
            tokenMap.put("token", "Bearer" + token);
            return ResponseHandle.SUCCESS(tokenMap);
        }
        return ResponseHandle.FAIL(HttpCode.USER_DISABLE);
    }
}

3. Query users by username

@Service
public class UserServiceImpl implements UserDetailsService {
    
    

    @Autowired
    private UserRepository userRepository;

    /**
     *
     * @param s
     * @return 实现loadUserByUsername方法,根据用户名查找用户信息
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    
    
        User user = userRepository.findByUsername(s);
        return new JwtUser(user);
    }
}

4. JPA realizes database query user

public interface UserRepository extends CrudRepository<User, Integer> {
    
    
    /**
     * 根据用户名查询用户
     * @param username
     * @return
     */
    User findByUsername(String username);
}

5. User entity

@Data
@Entity
@Table(name = "t_user")
public class User {
    
    

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "username")
    private String username;

    @Column(name = "password")
    private String password;

    @Column(name = "code")
    private String code;

    @Column(name = "role")
    private String role;

    @Override
    public String toString() {
    
    
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", role='" + role + '\'' +
                '}';
    }
}

3. Security configuration

Configure the security user source, authentication mode, filter, etc. Here we use our own login interface, so we need to let it go, and do not use the form submission mode

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Autowired
    private UserServiceImpl userService;

    @Resource
    private RestAuthorizationEntryPoint restAuthorizationEntryPoint;

    @Resource
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;



    /**
     * 常用的三种存储方式,项目找那个用的最多的为,自定义用户存储
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        //1、内存用户配置
//        auth.inMemoryAuthentication().passwordEncoder(bCryptPasswordEncoder())
//                .withUser("admin").password(bCryptPasswordEncoder().encode("123456")).authorities("ADMIN")
//                .and()
//                .withUser("test").password(bCryptPasswordEncoder().encode("123456")).authorities("TEST");
        //2、数据库用户配置
//        auth.jdbcAuthentication().dataSource(dataSource).passwordEncoder(passwordEncoder())
//                .usersByUsernameQuery(
//                        "select username, password, status from Users where username = ?")
//                .authoritiesByUsernameQuery(
//                        "select username, authority from Authority where username = ?");
        //3、自定义用户存储
        auth.userDetailsService(userService)
                .passwordEncoder(bCryptPasswordEncoder());
    }

    /**
     * configure(WebSecurity)用于影响全局安全性(配置资源,设置调试模式,通过实现自定义防火墙定义拒绝请求)的配置设置。
     * 一般用于配置全局的某些通用事物,例如静态资源等
     *
     * @param web
     */
    @Override
    public void configure(WebSecurity web) {
    
    
        web.ignoring()
                .antMatchers(HttpMethod.OPTIONS, "/**")  ///跨域请求预处理
                .antMatchers("/favicon.ico")
                .antMatchers("/swagger**")   // 以下swagger静态资源、接口不拦截
                .antMatchers("/doc.html")
                .antMatchers("/swagger-resources/**")
                .antMatchers("/v2/api-docs")
                .antMatchers("/webjars/**")
//                .antMatchers("/logout")
                .antMatchers("/js/**", "/css/**", "/images/**");  // 排除html静态资源
    }

    /**
     * 配置接口拦截
     * configure(HttpSecurity)允许基于选择匹配在资源级配置基于网络的安全性,
     * 也就是对角色所能访问的接口做出限制
     *
     * @param httpSecurity 请求属性
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
    
    
        httpSecurity
                .authorizeRequests()
                .antMatchers(HttpMethod.GET, "/demo/get").permitAll()
                .antMatchers("/oak/login", "/oak/logout").permitAll()
                //指定权限为ROLE_ADMIN才能访问,这里和方法注解配置效果一样,但是会覆盖注解
                .antMatchers("/demo/delete").hasRole("ADMIN")
                // 所有请求都需要验证
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthorizationEntryPoint)
                .and()
                .csrf().disable()
                .sessionManagement()//禁用session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .logout().logoutUrl("/logout")
                .and()
                // 禁用缓存
                .headers()
                .cacheControl();
    }


    /**
     * 配置用户认证方式
     *
     * @return
     * @throws Exception
     */
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
    
    
        return super.authenticationManagerBean();
    }


    /**
     * 自定义过滤器,用来替换security的默认过滤器(UsernamePasswordAuthenticationFilter),
     * 实现自定义的login接口,接口路径为了区别默认的/login我们定义为/mylogin
     *
     * @return
     * @throws Exception
     */

    /**
     * 使用security 提供的加密规则(还有其他加密方式)
     *
     * @return
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
    
    
        return new BCryptPasswordEncoder();
    }

    /**
     * JWT token过滤器
     * @return
     * @throws Exception
     */
    @Bean
    public JwtAuthencationTokenFilter jwtAuthenticationFilter() throws Exception {
    
    
        JwtAuthencationTokenFilter jwtAuthenticationFilter = new JwtAuthencationTokenFilter(authenticationManager());
        return jwtAuthenticationFilter;
    }
}

4. Customize the implementation of JwtAuthencationTokenFilter

Configure security user sources, authentication modes, filters, etc. Here we define our own /mylogin login interface

public class JwtAuthencationTokenFilter extends BasicAuthenticationFilter {
    
    
    private String tokenHeader = "Authorization";
    private String tokenHead = "Bearer";

    @Autowired
    private UserServiceImpl userService;

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

    /**
     * 自定义过滤器,用来校验token是否存在,token是否失效
     *
     * @param request
     * @param response
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
    
    
        // 请求头中获取token信息
        String authheader = request.getHeader(tokenHeader);
        // 存在token
        if (null != authheader && authheader.startsWith(tokenHead)) {
    
    
            // 去除字段名称, 获取真正token
            String authToken = authheader.substring(tokenHead.length());
            // 利用token获取用户名
            String username = JwtTokenUtils.getUsername(authToken);

            System.out.println("自定义JWT过滤器获得用户名为" + username);
            Authentication a = SecurityContextHolder.getContext().getAuthentication();

            // token存在用户未登陆
            // SecurityContextHolder.getContext().getAuthentication() 获取上下文对象中认证信息
            if (null != username && null == SecurityContextHolder.getContext().getAuthentication()) {
    
    
                // 自定义数据源获取用户信息
                UserDetails userDetails = userService.loadUserByUsername(username);
                // 验证token是否有效 验证token用户名和存储的用户名是否一致以及是否在有效期内, 重新设置用户对象
//                if (JwtTokenUtils.isExpiration(authToken)) {
    
    
                // 重新将用户信息封装到UsernamePasswordAuthenticationToken
                if (JwtTokenUtils.checkToken(authToken)) {
    
    
                    System.out.println("token有效");
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        //继续下一个拦截器
        filterChain.doFilter(request, response);
    }
}

4. JwtTokenUtils tool class

It is mainly used to configure and generate tokens, verify tokens and other related methods

public class JwtTokenUtils {
    
    


    private static final String SECRET = "oak-secret";
    private static final String ISS = "oak";

    /**
     * 角色的key
     */
    private static final String ROLE_CLAIMS = "rol";

    /**
     * 过期时间是3600秒,既是1个小时
     */
    private static final long EXPIRATION = 3600L;

    /**
     * 选择了记住我之后的过期时间为7天
     */
    private static final long EXPIRATION_REMEMBER = 604800L;

    /**
     * 创建token
     *
     * @param username
     * @param role
     * @param isRememberMe
     * @return
     */
    public static String createToken(String username, String role, boolean isRememberMe) {
    
    
        long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
        HashMap<String, Object> map = new HashMap<>();
        map.put(ROLE_CLAIMS, role);
        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .setClaims(map)  //存放自定义信息,也可不放
                .setIssuer(ISS)     //发行人
                .setSubject(username)   //jwt主题
                .setIssuedAt(new Date())   //当前时间
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)) // 过期时间
                .compact();
    }

    /**
     * 从token中获取用户名
     *
     * @param token
     * @return
     */
    public static String getUsername(String token) {
    
    
        return getTokenBody(token).getSubject();
    }


    /**
     * 获取用户角色
     *
     * @param token
     * @return
     */
    public static String getUserRole(String token) {
    
    
        return (String) getTokenBody(token).get(ROLE_CLAIMS);
    }

    /**
     * 是否已过期
     *
     * @param token
     * @return
     */
    public static boolean isExpiration(String token) {
    
    
        try {
    
    
            return getTokenBody(token).getExpiration().before(new Date());
        } catch (ExpiredJwtException e) {
    
    
            return true;
        }
    }


    /**
     * 根据token,判断token是否存在与有效
     *
     * @param jwtToken
     * @return
     */
    public static boolean checkToken(String jwtToken) {
    
    
        if (StringUtils.isEmpty(jwtToken)) return false;
        try {
    
    
            Jwts.parser().setSigningKey(SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return false;
        }
        return true;
    }

    private static Claims getTokenBody(String token) {
    
    
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody();
    }
}

4. Verify login

1. Write the test interface

@Api(tags = {
    
    "演示相关接口"})
@RestController
@RequestMapping("/demo")
public class DemoCtrl {
    
    

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;


    @ApiOperation(value = "获取接口", notes = "获取接口")
    @GetMapping(value = "/get")
    public ResponseHandle get() {
    
    
        return ResponseHandle.SUCCESS("获取数据成功");
    }

    @ApiOperation(value = "更新接口(ADMIN可访问)", notes = "更新接口")
    @GetMapping(value = "/update1")
    @PreAuthorize("hasAnyRole('ROLE_ADMIN')")
    public ResponseHandle update1() {
    
    
        return ResponseHandle.SUCCESS("更新数据成功");
    }

    @ApiOperation(value = "查询接口(USER可访问)", notes = "查询接口")
    @GetMapping(value = "/find")
    @PreAuthorize("hasAnyRole('ROLE_USER')")
    public ResponseHandle find() {
    
    
        return ResponseHandle.SUCCESS("查询数据成功");
    }

    @ApiOperation(value = "删除用户(ADMIN配置可用)", notes = "修改")
    @GetMapping("/delete")
    public ResponseHandle delete() {
    
    
        return ResponseHandle.SUCCESS("删除成功");
    }

    @ApiOperation(value = "注册用户", notes = "注册")
    @PostMapping("/register")
    public String registerUser(@RequestBody Map<String, String> registerUser) {
    
    
        User user = new User();
        user.setUsername(registerUser.get("username"));
        user.setPassword(bCryptPasswordEncoder.encode(registerUser.get("password")));
        user.setRole("ROLE_USER");
        User save = userRepository.save(user);
        return save.toString();
    }
}

2. Call interface prompt

1. Call the login call interface.
Please add a picture description
You can see that the interface returns a long string of tokens. Next time we access service resources, we only need to bring the changed token.

2. Call the delete interface
Please add a picture description> 3. Change to a user whose role is USER to call the delete interfacePlease add a picture description

Summarize

At this point, springboot integrates security to complete the user's database configuration and custom login, and improves the login-related responses, which facilitates the unified processing of the front and back ends. This kind of login can meet the needs of a single project, but considering the login authentication between multiple projects, there are still many problems in this solution. Next, we will continue to complete the authentication method based on OAuth2 token

Links to Chapter 1: SpringBoot Integrated Security (1) | (Introduction to Security)
Links to Chapter 2: SpringBoot Integrated Security (2) | (Security Custom Configuration)
Links to Chapter 3: SpringBoot Integrated Security (3) | (Before and After Security End-separated login and response processing)
Chapter 4 link: SpringBoot integrated security (4) | (Security is based on JWT to achieve front-end separation and custom login)

Guess you like

Origin blog.csdn.net/Oaklkm/article/details/128190531