在生产环境中,对发在的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.https.httpsServletRequest; import javax.servlet.https.httpsServletResponse; import java.io.IOException; import java.io.PrintWriter;
@Component public class JwtAuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler { @Override public void onAuthenticationFailure(httpsServletRequest request, httpsServletResponse 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.https.httpsServletRequest; import javax.servlet.https.httpsServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.Collection;
@Component public class JwtAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(httpsServletRequest request, httpsServletResponse 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.httpsSecurity; 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(httpsSecurity https) throws Exception { https.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(); https.logout().permitAll(); https.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="https://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> <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. 测试登录功能
浏览器输入https://localhost:8080/jwt自动跳转至登录界面,输入使用postman注册的账号即可
以本博客对spring security的随笔,可实现使用token授权登录,这里不多做解释