在生产环境中,对发在的API增加授权保护是非常必要的。JWT作为一个无状态的授权校捡技术,非常适合于分布式系统架构。服务器端不需要保存用户状态,因此,无须采用Redis等技术来实现各个服务节点之间共享Session数据。

  本节通过实例讲解如何用JWT技术进行授权认证和保护。

  1.1 配置安全类

  (1)自定义用户

查看代码

 package com.intehel.jwt.domain;

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Entity
@Data
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean accountNonExpired;
    private Boolean accountNonLocked;
    private Boolean credentialsNonExpired;
    @ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.PERSIST)
    private List<Role> roles;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
    @Override
    public String getPassword() {
        return password;
    }
    @Override
    public String getUsername() {
        return username;
    }
    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }
    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }
    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

  (2)自定义角色

查看代码

 package com.intehel.jwt.domain;

import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Data
@Entity(name = "role")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
    private String nameZh;
}

  (3)JPA

package com.intehel.jwt.repository;

import com.intehel.jwt.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User,Integer> {
    User findUserByUsername(String username);
}
package com.intehel.jwt.repository;
import com.intehel.jwt.domain.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface UserRoleRepository extends JpaRepository<Role,Integer> {
    Role findByName(String name);
}

  (4)认证失败和认证成功处理器

查看代码

package com.intehel.jwt.handler;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @Author:李自航
 * @Description:
 * @CreateDate:2022/8/5 10:15
 * @UPdateDate:2022/8/5 10:15
 * @Version:版本号
 */

@Component
public class JwtAuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        request.setCharacterEncoding("UTF-8");
        String name = request.getParameter("name");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write("{\n" +
                "\t\"status\":\"error\",\n" +
                "\t\"message\":\"用户名或密码错误\"\n" +
                "}");
        out.flush();
        out.close();
    }
}

查看代码

 package com.intehel.jwt.handler;


import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;

@Component
public class JwtAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if (principal != null && principal instanceof UserDetails){
            UserDetails user = (UserDetails) principal;
            request.getSession().setAttribute("userDetail",user);
            String role = "";
            Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
            for (GrantedAuthority authority : authorities){
                role = authority.getAuthority();
            }
            String token = "灌水灌水";
            response.setHeader("token",token);
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            out.write("{\n" +
                    "\t\"status\":\"ok\",\n" +
                    "\t\"message\":\"登录成功\"\n" +
                    "}\n");
            out.flush();
            out.close();
        }

    }
}

  (5)配置安全类

package com.intehel.jwt.config;

import com.intehel.jwt.handler.JwtAuthenticationFailHandler;
import com.intehel.jwt.handler.JwtAuthenticationSuccessHandler;
import com.intehel.jwt.handler.MyAuthenticationFailureHandler;
import com.intehel.jwt.handler.MyAuthenticationSuccessHandler;
import com.intehel.jwt.service.MyUserDetailsService;
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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Autowired
    private JwtAuthenticationFailHandler myAuthenticationFailureHandler;
    @Autowired
    MyUserDetailsService jwtDetailsService;
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/jwt/**")
                .formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/doLogin")
                .loginPage("/mylogin.html")
                .successHandler(myAuthenticationSuccessHandler)
                .failureHandler(myAuthenticationFailureHandler)
                .and()
                .authorizeRequests()
                .antMatchers("/register/mobile").permitAll()
                .antMatchers("/article/**").authenticated()
                .antMatchers("/jwt/tasks/**").hasRole("USER")
                .anyRequest().authenticated()
                .and()
                .csrf().disable();
        http.logout().permitAll();
        http.cors().and().csrf().ignoringAntMatchers("/jwt/**");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(jwtDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/jwt/register/mobile");

    }
}

  从上面代码可以看出,此处JWT的安全配置和上面已经讲解过的安全配置并无区别,没有特别的参数需要配置。

  1.2 自定义登录界面

查看代码

 <!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
    <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<style>
    #login .container #login-row #login-column #login-box {
        border: 1px solid #9c9c9c;
        background-color: #EAEAEA;
    }
</style>
<body>
<div id="login">
    <div class="container">
        <div id="login-row" class="row justify-content-center align-items-center">
            <div id="login-column" class="col-md-6">
                <div id="login-box" class="col-md-12">
                    <form id="login-form" class="form" action="/doLogin" method="post">
                        <h3 class="text-center text-info">登录</h3>
                        <!--/*@thymesVar id="SPRING_SECURITY_LAST_EXCEPTION" type="com"*/-->
                        <div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>
                        <div class="form-group">
                            <label for="username" class="text-info">用户名:</label><br>
                            <input type="text" name="username" id="username" class="form-control">
                        </div>
                        <div class="form-group">
                            <label for="password" class="text-info">密码:</label><br>
                            <input type="text" name="password" id="password" class="form-control">
                        </div>
                        <div class="form-group">
                            <input type="submit" name="submit" class="btn btn-info btn-md" value="登录">
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

  1.3 处理注册

  在注册时为了安全,需要将注册的密码经过加密再写入数据库中。

   spring security 5之后,需要对密码添加这个类型(id),可参考文章www.cnblogs.com/majianming/p/7923604.html

  

查看代码

 package com.intehel.jwt.controller;

import com.intehel.jwt.domain.Role;
import com.intehel.jwt.domain.User;
import com.intehel.jwt.repository.UserRepository;
import com.intehel.jwt.repository.UserRoleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/jwt")
public class JwtUserController {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private UserRoleRepository userRoleRepository;
    @RequestMapping(value = "/register/mobile")
    public String register(User user){
        try {
            User userName = userRepository.findUserByUsername(user.getUsername());
            if (userName != null){
                return "用户名已存在";
            }
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
            user.setPassword("{bcrypt}"+encoder.encode(user.getPassword()));
            List<Role> roles = new ArrayList<>();
            Role role = userRoleRepository.findByName("ROLE_admin");
            roles.add(role);
            user.setRoles(roles);
            userRepository.save(user);
        }catch (Exception e){
            return "出现了异常";
        }
        return "成果";
    }
}

  1.4 处理登录

查看代码

 package com.intehel.jwt.service;

import com.intehel.jwt.domain.User;
import com.intehel.jwt.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
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;

@Service
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    UserRepository userRepository;
    @Override
    public UserDetails loadUserByUsername(String username)throws UsernameNotFoundException {
        User user = userRepository.findUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return user;
    }
}

  测试多方式注册和登录

  1.测试注册功能

  这里使用测试工具Postman提交POST注册请求

  

  数据库插入信息如下

  

  2. 测试登录功能

  浏览器输入http://localhost:8080/jwt自动跳转至登录界面,输入使用postman注册的账号即可

  以本博客对spring security的随笔,可实现使用token授权登录,这里不多做解释