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 {(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;// 创建tokenpublic 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
public class UserDetailsServiceImpl implements UserDetailsService {private UserDao userDao;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()));}// 获取权限信息,目前博主只会拿来存角色。。public Collection<? extends GrantedAuthority> getAuthorities() {return authorities;}public String getPassword() {return password;}public String getUsername() {return username;}// 账号是否未过期,默认是false,记得要改一下public boolean isAccountNonExpired() {return true;}// 账号是否未锁定,默认是false,记得也要改一下public boolean isAccountNonLocked() {return true;}// 账号凭证是否未过期,默认是false,记得还要改一下public boolean isCredentialsNonExpired() {return true;}// 这个有点抽象不会翻译,默认也是false,记得改一下public boolean isEnabled() {return true;}// 我自己重写打印下信息看的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;}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并返回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);}// 这是验证失败时候调用的方法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);}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中获取用户信息并新建一个tokenprivate 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
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {// 因为UserDetailsService的实现类实在太多啦,这里设置一下我们要注入的实现类("userDetailsServiceImpl")private UserDetailsService userDetailsService;// 加密密码的,安全第一嘛~public BCryptPasswordEncoder bCryptPasswordEncoder(){return new BCryptPasswordEncoder();}protected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());}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);}CorsConfigurationSource corsConfigurationSource() {final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());return source;}}
四、测试
4.1 注册
AuthController.java
public class AuthController {// 为了减少篇幅就不写service接口了private UserDao userDao;private BCryptPasswordEncoder bCryptPasswordEncoder;public String registerUser( 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
public class HelloController {public String hello() {return "hello jwt !";}public String admin() {return "hello admin !";}}
4.4 测试结果
先是注册

登陆
这是可以获取 token

接口访问测试

需要将 token 加上才可以访问成功。
记得点「赞」和「在看」↓
爱你们
