Spring-Security & JWT 实现 token

简介: Spring-Security & JWT 实现 token

一、JWT

//www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

二、项目环境搭建

2.1 引入依赖

pom.xml

<dependencies>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-core</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
        </dependency>
        <!-- spring 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>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- mybatisplus与springboot整合 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>
        <!-- mybatis plus 代码生成器依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.2.0</version>
        </dependency>
        <!--swagger-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

主要是要引入Spring Secruity和jwt的依赖。

2.2 实体类(User)

User.java

public class User implements Serializable {
    @TableId(value = "user_id",type= IdType.AUTO)
    private int userId;
    private String userName;
    private String Password;
    private String userAge;
    private String Role;
    public int getUserId() {
        return userId;
    }
    public void setUserId(int userId) {
        this.userId = userId;
    }
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String getPassword() {
        return Password;
    }
    public void setPassword(String password) {
        Password = password;
    }
    public String getUserAge() {
        return userAge;
    }
    public void setUserAge(String userAge) {
        this.userAge = userAge;
    }
    public String getRole() {
        return Role;
    }
    public void setRole(String role) {
        Role = role;
    }
}

2.3 jwt工具类

JwtTokenUtils.java

public class JwtTokenUtils {
    public static final String TOKEN_HEADER = "token";
    public static final String TOKEN_PREFIX = "";
    private static final String SECRET = "jwtsecretdemo";
    private static final String ISS = "echisan";
    // 过期时间是3600秒,即是1个小时
    private static final long EXPIRATION = 3600L;
    // 选择了记住我之后的过期时间为7天
    private static final long EXPIRATION_REMEMBER = 604800L;
    // 创建token
    public static String createToken(String username, boolean isRememberMe) {
        long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .setIssuer(ISS)
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
                .compact();
    }
    // 从token中获取用户名
    public static String getUsername(String token){
        return getTokenBody(token).getSubject();
    }
    // 是否已过期
    public static boolean isExpiration(String token){
        return getTokenBody(token).getExpiration().before(new Date());
    }
    private static Claims getTokenBody(String token){
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody();
    }
}

jwt工具类,对jjwt封装一下方便调用。

2.4 Dao层

因为使用的是mybatis plus,所以没有使用mapper.xml书写sql语句,直接调用提供的CRUD。

UserDao.java

public interface UserDao extends BaseMapper<User> {
}

2.5 ServiceImpl层

UserDetailsServiceImpl.java

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserDao userDao;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        Map<String, Object> map = new HashMap<>();
        map.put("user_name", s);
        User user = userDao.selectByMap(map).get(0);
        return new JwtUser(user);
    }
}
  • 注意:这个serviceImpl实现的接口UserDetailsService是框架提供的。
  • 使用springSecurity需要实现UserDetailsService接口供权限框架调用,该方法只需要实现一个方法就可以了,那就是根据用户名去获取用户,这里使用的是mybatis plus提供的操作接口。

接着去实现一下刚才返回的UserDetails

public class JwtUser implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;
    public JwtUser() {
    }
    // 写一个能直接使用user创建jwtUser的构造器
    public JwtUser(User user) {
        id = user.getUserId();
        username = user.getUserName();
        password = user.getPassword();
        authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole()));
    }
    // 获取权限信息,目前博主只会拿来存角色。。
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }
    @Override
    public String getPassword() {
        return password;
    }
    @Override
    public String getUsername() {
        return username;
    }
    // 账号是否未过期,默认是false,记得要改一下
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    // 账号是否未锁定,默认是false,记得也要改一下
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    // 账号凭证是否未过期,默认是false,记得还要改一下
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    // 这个有点抽象不会翻译,默认也是false,记得改一下
    @Override
    public boolean isEnabled() {
        return true;
    }
    // 我自己重写打印下信息看的
    @Override
    public String toString() {
        return "JwtUser{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", authorities=" + authorities +
                '}';
    }
}

三、配置拦截器

这边需要实现两个过滤器。使用JWTAuthenticationFilter去进行用户账号的验证,使用JWTAuthorizationFilter去进行用户权限的验证。

3.1 JWTAuthenticationFilter

JWTAuthenticationFilter继承于UsernamePasswordAuthenticationFilter

该拦截器用于获取用户登录的信息,只需创建一个token并调用authenticationManager.authenticate()让spring-security去进行验证就可以了,不用自己查数据库再对比密码了,这一步交给spring去操作。

这个操作有点像是shiro的subject.login(new UsernamePasswordToken()),验证的事情交给框架。

JWTAuthenticationFilter.java

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private AuthenticationManager authenticationManager;
    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        // 从输入流中获取到登录的信息
        try {
            LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class);
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>())
            );
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
    // 成功验证后调用的方法
    // 如果验证成功,就生成token并返回
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        // 查看源代码会发现调用getPrincipal()方法会返回一个实现了`UserDetails`接口的对象
        // 所以就是JwtUser啦
        JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
        System.out.println("jwtUser:" + jwtUser.toString());
        String token = JwtTokenUtils.createToken(jwtUser.getUsername(), false);
        // 返回创建成功的token
        // 但是这里创建的token只是单纯的token
        // 按照jwt的规定,最后请求的格式应该是 `Bearer token`
        response.setHeader("token", JwtTokenUtils.TOKEN_PREFIX + token);
    }
    // 这是验证失败时候调用的方法
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.getWriter().write("authentication failed, reason: " + failed.getMessage());
    }
}

这里还用到了LoginUser这个实体类,也是需要自己定义一下的。

LoginUser.java

public class LoginUser {
    private String username;
    private String password;
    private Integer rememberMe;
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public Integer getRememberMe() {
        return rememberMe;
    }
    public void setRememberMe(Integer rememberMe) {
        this.rememberMe = rememberMe;
    }
}

3.2 JWTAuthorizationFilter

验证成功当然就是进行鉴权了,每一次需要权限的请求都需要检查该用户是否有该权限去操作该资源,当然这也是框架帮我们做的,那么我们需要做什么呢?很简单,只要告诉spring-security该用户是否已登录,是什么角色,拥有什么权限就可以了。

JWTAuthenticationFilter继承于BasicAuthenticationFilter,至于为什么要继承这个我也不太清楚了,这个我也是网上看到的其中一种实现,实在springSecurity苦手,不过我觉得不继承这个也没事呢(实现以下filter接口或者继承其他filter实现子类也可以吧)只要确保过滤器的顺序,JWTAuthorizationFilter在JWTAuthenticationFilter后面就没问题了。

JWTAuthorizationFilter.java

public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
    public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws IOException, ServletException {
        String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
        // 如果请求头中没有Authorization信息则直接放行了
        if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {
            chain.doFilter(request, response);
            return;
        }
        // 如果请求头中有token,则进行解析,并且设置认证信息
        SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
        super.doFilterInternal(request, response, chain);
    }
    // 这里从token中获取用户信息并新建一个token
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
        String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
        String username = JwtTokenUtils.getUsername(token);
        if (username != null){
            return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
        }
        return null;
    }
}

3.3 配置SpringSecurity

需要开启一下注解@EnableWebSecurity然后再继承一下WebSecurityConfigurerAdapter就可以啦

WebSecurityConfig.java

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    // 因为UserDetailsService的实现类实在太多啦,这里设置一下我们要注入的实现类
    @Qualifier("userDetailsServiceImpl")
    private UserDetailsService userDetailsService;
    // 加密密码的,安全第一嘛~
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .authorizeRequests()
                // 测试用资源,需要验证了的用户才能访问
                .antMatchers("/tasks/**").authenticated()
                // 其他都放行了
                .anyRequest().permitAll()
                .and()
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                .addFilter(new JWTAuthorizationFilter(authenticationManager()))
                // 不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }
}

四、测试

4.1 注册

AuthController.java

@RestController
@RequestMapping("/auth")
public class AuthController {
    // 为了减少篇幅就不写service接口了
    @Autowired
    private UserDao userDao;
    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    @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");
        int result = userDao.insert(user);
        return Integer.toString(result);
    }
}

4.2 登陆

根据UsernamePasswordAuthenticationFilter的源代码,可以看出登录默认是/login

public UsernamePasswordAuthenticationFilter() {
    super(new AntPathRequestMatcher("/login", "POST"));
}

当然也可以自定义,只需要在JWTAuthenticationFilter的构造方法中加入下面那一句话就可以啦

public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        super.setFilterProcessesUrl("/auth/login");
}

4.3 接口验证

helloController.java

@RestController
@RequestMapping("/tasks")
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello jwt !";
    }
    @GetMapping("/admin")
    public String admin() {
        return "hello admin !";
    }
}

4.4 测试结果

  • 先是注册

  • 登陆

这是可以获取 token


  • 接口访问测试

需要将 token 加上才可以访问成功。


相关文章
|
3月前
|
SQL Java 测试技术
在Spring boot中 使用JWT和过滤器实现登录认证
在Spring boot中 使用JWT和过滤器实现登录认证
222 0
|
25天前
|
JSON 安全 算法
|
4天前
|
JSON 安全 Go
Go语言中使用JWT鉴权、Token刷新完整示例,拿去直接用!
本文介绍了如何在 Go 语言中使用 Gin 框架实现 JWT 用户认证和安全保护。JWT(JSON Web Token)是一种轻量、高效的认证与授权解决方案,特别适合微服务架构。文章详细讲解了 JWT 的基本概念、结构以及如何在 Gin 中生成、解析和刷新 JWT。通过示例代码,展示了如何在实际项目中应用 JWT,确保用户身份验证和数据安全。完整代码可在 GitHub 仓库中查看。
18 1
|
25天前
|
存储 安全 Java
|
4月前
|
JSON 安全 Java
使用Spring Boot和JWT实现用户认证
使用Spring Boot和JWT实现用户认证
|
2月前
|
存储 中间件 API
ThinkPHP 集成 jwt 技术 token 验证
本文介绍了在ThinkPHP框架中集成JWT技术进行token验证的流程,包括安装JWT扩展、创建Token服务类、编写中间件进行Token校验、配置路由中间件以及测试Token验证的步骤和代码示例。
ThinkPHP 集成 jwt 技术 token 验证
|
15天前
|
JavaScript NoSQL Java
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
30 0
|
2月前
|
JSON 安全 数据安全/隐私保护
从0到1搭建权限管理系统系列三 .net8 JWT创建Token并使用
【9月更文挑战第22天】在.NET 8中,从零开始搭建权限管理系统并使用JWT(JSON Web Tokens)创建Token是关键步骤。JWT是一种开放标准(RFC 7519),用于安全传输信息,由头部、载荷和签名三部分组成。首先需安装`Microsoft.AspNetCore.Authentication.JwtBearer`包,并在`Program.cs`中配置JWT服务。接着,创建一个静态方法`GenerateToken`生成包含用户名和角色的Token。最后,在控制器中使用`[Authorize]`属性验证和解析Token,从而实现身份验证和授权功能。
131 3
|
3月前
|
存储 JSON JavaScript
震撼!Cookie、Session、Token、JWT 终极对决:揭开 Web 认证的神秘面纱!
【8月更文挑战第13天】Web 开发中,Cookie、Session、Token 和 JWT 常混淆。Cookie 是服务器给客户端的小信息片,如登录状态,每次请求都会返回。Session 则是服务器存储的用户数据,通过 Session ID 追踪。Token 类似通行证,证明客户端身份且可加密。JWT 是结构化的 Token,含头部、载荷及签名,确保数据完整性和安全性。
68 4
|
3月前
|
API
【Azure Developer】记录一段验证AAD JWT Token时需要设置代理获取openid-configuration内容
【Azure Developer】记录一段验证AAD JWT Token时需要设置代理获取openid-configuration内容