security+JWT
- 添加依赖
- 准备工作
- sql
- UserInfo
- UserMapper
- UserService、UserServiceImpl
- 创建JwtUtils工具类,做token的生成和校验
- 进入Security
- 创建AccountDetailsServiceImpl,并且实现UserDetailsService编写登录操作
- 创建拦截器JWTAuthenticationFilter继承UsernamePasswordAuthenticationFilter
- 创建拦截器JWTAuthorizationFilter
- 创建WebSecurityConfig配置类,对springsecurity进行相关配置
- 创建JWTAuthenticationEntryPoint
- 测试
- 访问/user/add
- 登录/api/login
- 由于使用的是调试工具测试的返回的token我也不知道在哪,可以通过代码输出进行获取
- 在/user/add接口Hander中添加token
- 再次访问/user/add,添加成功
- 上面代码是使用的默认的/api/login的登录接口
添加依赖
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.8</version>
</dependency>
<!--lombok用来简化实体类-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.5.14</version>
</dependency>
<dependency>
<groupId>com.fawkes</groupId>
<artifactId>fawkes-core</artifactId>
<version>2.0.0-RELEASE</version>
</dependency>
<!-- jwt增强-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- alibaba json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
准备工作
sql
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`salt` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
UserInfo
package com.wlj.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
@Data
@TableName("user")
public class UserInfo implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id",type = IdType.AUTO)
private Integer id;
private String username;
private String password;
private String salt;
}
UserMapper
package com.wlj.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.wlj.entity.UserInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<UserInfo> {
}
UserService、UserServiceImpl
package com.wlj.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.wlj.entity.UserInfo;
public interface UserService extends IService<UserInfo> {
String login(UserInfo user);
String add(UserInfo user);
String findOne(UserInfo user);
String update(UserInfo user);
}
package com.wlj.service;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wlj.entity.UserInfo;
import com.wlj.mapper.UserMapper;
import com.wlj.util.Constant;
import com.wlj.util.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserInfo> implements UserService {
@Autowired
UserMapper userMapper;
@Override
public String login(UserInfo user) {
UserInfo sysUser = userMapper.selectOne(
new QueryWrapper<UserInfo>().lambda().eq(UserInfo::getUsername, user.getUsername()));
String token = JwtTokenUtil.createToken(sysUser.getUsername(), Constant.PRIORITY_FRONT_USER);
JSONObject jsonObject = new JSONObject();
jsonObject.put("Authorization", JwtTokenUtil.TOKEN_PREFIX + token);
jsonObject.put("userName", sysUser.getUsername());
return jsonObject.toString();
}
@Override
public String add(UserInfo user) {
int insert = userMapper.insert(user);
return insert +"";
}
@Override
public String findOne(UserInfo user) {
UserInfo sysUser = userMapper.selectOne(
new QueryWrapper<UserInfo>().lambda().eq(UserInfo::getUsername, user.getUsername()));
return sysUser.getUsername();
}
@Override
public String update(UserInfo user) {
QueryWrapper<UserInfo> wrapper = new QueryWrapper<>();
wrapper.eq("id", user.getId());
int update = userMapper.update(user, wrapper);
return "成功更新"+ update+ "条数据";
}
}
创建JwtUtils工具类,做token的生成和校验
package com.wlj.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtTokenUtil {
// Token请求头
public static final String TOKEN_HEADER = "Authorization";
// Token前缀
public static final String TOKEN_PREFIX = "Bearer ";
// 签名主题
public static final String SUBJECT = "piconjo";
// 过期时间
public static final long EXPIRITION = 1000 * 24 * 60 * 60 * 7;
//public static final long EXPIRITION = 1000 * 60;
// 应用密钥
public static final String APPSECRET_KEY = "Yik8MJeqpcO97HX1+WM1o38pG6ln0otV0O/WS6eVcF/uQNLUX9IJY6lRMnJh 9jdJjlBAmfhMPZ5GOyuji39RwWU7OQse/iEfY5fgE/eTZPCWqJpDykqtVYJ+ ZWV1Y6Tk+U8EilEuDCw4uAmYYGvH3oaM0scWzat9TJvvdFRt1E8=";
// 角色权限声明
private static final String ROLE_CLAIMS = "role";
private static final String ISS = "paperpass";
// @Autowired
// private static RedisUtils redisUtils;
/**
* 生成Token
*/
public static String createToken(String username, String role) {
Map<String, Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, "ROLE_" + role);
String token = Jwts
.builder()
.setSubject(username)
.setClaims(map)
.claim("username", username)
.setIssuer(ISS)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRITION))
.signWith(SignatureAlgorithm.HS256, APPSECRET_KEY)
.compact();
System.out.println("token失效时间:" + new Date(System.currentTimeMillis() + EXPIRITION));
return token;
}
/**
* 校验Token
*/
public static Claims checkJWT(String token) {
try {
return Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 从Token中获取username
*/
public static String getUsername(String token) {
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims.get("username").toString();
}
/**
* 从Token中获取用户角色
*/
public static String getUserRole(String token) {
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims.get("role").toString();
}
/**
* 校验Token是否过期
*/
public static boolean isExpiration(String token) {
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims.getExpiration().before(new Date());
}
public static void refreshTokenExpiration(String token) {
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
claims.setExpiration(new Date(System.currentTimeMillis() + EXPIRITION));
System.out.println("token失效时间:" + new Date(System.currentTimeMillis() + EXPIRITION));
}
}
进入Security
创建AccountDetailsServiceImpl,并且实现UserDetailsService编写登录操作
package com.wlj.config;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wlj.entity.UserInfo;
import com.wlj.mapper.UserMapper;
import com.wlj.util.Constant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
@Service
public class AccountDetailsServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Autowired
PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
if (s == null || "".equals(s)) {
throw new RuntimeException("用户不能为空");
}
// 调用方法查询用户
UserInfo sysUser = userMapper.selectOne(
new QueryWrapper<UserInfo>().lambda().eq(UserInfo::getUsername, s));
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_" + Constant.PRIORITY_FRONT_USER));
//密码加密格式 和前端的验证方法一致
String encode = passwordEncoder.encode(sysUser.getPassword());
return new User(sysUser.getUsername(),encode, authorities);
}
}
UserDetails和UserDetailsService是什么?
两个接口
UserDetails:提供用户核心信息,登录验证完成之后,根据上述代码 return new User(sysUser.getUsername(),encode, authorities); security会保存用户的信息,通过 authResult.getPrincipal();可以获取到的对应用户信息和权限信息
UserDetailsService:用户自定义登录逻辑。需要设置使用默认的登录
接口
创建拦截器JWTAuthenticationFilter继承UsernamePasswordAuthenticationFilter
springsercurity对UserDetailsServiceImpl返回的user进行验证,验证成功则生成token,失败则返回失败信息
UsernamePasswordAuthenticationFilter 用户密码认证拦截器
如何获取用户输入的账号密码
UsernamePasswordAuthenticationFilter扩展AbstractAuthenticationProcessingFilter,因为需要从HTTP请求中从指定名称的参数获取用户名和密码,并且传递给验证核心;
package com.wlj.util;
import com.alibaba.fastjson.JSON;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;
/**
* 验证用户名密码正确后 生成一个token并将token返回给客户端
* <p>
* 该拦截器用于获取用户登录的信息
* 至于具体的验证 只需创建一个token并调用authenticationManager的authenticate()方法
* 让Spring security验证即可 验证的事交给框架
*/
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
//配置登录验证的url 会通过AccountDetailsServiceImpl的loadUserByUsername继续验证,验证通过会进入successfulAuthentication方法 不通过会进入unsuccessfulAuthentication
super.setFilterProcessesUrl("/api/login");
}
/**
* 验证操作 接收并解析用户凭证
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 从输入流中获取到登录的信息
// 创建一个token并调用authenticationManager.authenticate() 让Spring security进行验证
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
request.getParameter("username"), request.getParameter("password")));
}
/**
* 验证【成功】后调用的方法
* 若验证成功 生成token并返回
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
User user = (User) authResult.getPrincipal();
// 从User中获取权限信息
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
// 创建Token
String token = JwtTokenUtil.createToken(user.getUsername(), authorities.toString());
// 设置编码 防止乱码问题
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
// 在请求头里返回创建成功的token
// 设置请求头为带有"Bearer "前缀的token字符串
response.setHeader("token", JwtTokenUtil.TOKEN_PREFIX + token);
// 处理编码方式 防止中文乱码
response.setContentType("text/json;charset=utf-8");
// 将反馈塞到HttpServletResponse中返回给前台
response.getWriter().write(JSON.toJSONString("登录成功"));
}
/**
* 验证【失败】调用的方法
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
String returnData = "";
// 账号过期
if (failed instanceof AccountExpiredException) {
returnData = "账号过期";
}
// 密码错误
else if (failed instanceof BadCredentialsException) {
returnData = "密码错误";
}
// 密码过期
else if (failed instanceof CredentialsExpiredException) {
returnData = "密码过期";
}
// 账号不可用
else if (failed instanceof DisabledException) {
returnData = "账号不可用";
}
//账号锁定
else if (failed instanceof LockedException) {
returnData = "账号锁定";
}
// 用户不存在
else if (failed instanceof InternalAuthenticationServiceException) {
returnData = "用户不存在";
}
// 其他错误
else {
returnData = "未知异常";
}
// 处理编码方式 防止中文乱码
response.setContentType("text/json;charset=utf-8");
// 将反馈塞到HttpServletResponse中返回给前台
response.getWriter().write(JSON.toJSONString(returnData));
}
}
创建拦截器JWTAuthorizationFilter
会对所有security没有放行的url进行拦截验证
package com.wlj.util;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
/**
* 登录成功后 走此类进行鉴权操作
* <p>
* 当访问需要权限校验的URL(当然 该URL也是需要经过配置的) 则会来到此拦截器 在该拦截器中对传来的Token进行校验
* 只需告诉Spring security该用户是否已登录 并且是什么角色 拥有什么权限即可
*/
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
// private RedisUtils redisUtils;
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
/**
* 在过滤之前和之后执行的事件
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String tokenHeader = request.getHeader(JwtTokenUtil.TOKEN_HEADER);
if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// 如果请求头中有token,则进行解析,并且设置认证信息
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
super.doFilterInternal(request, response, chain);
}
/**
* 从token中获取用户信息并新建一个token
*
* @param tokenHeader 字符串形式的Token请求头
* @return 带用户名和密码以及权限的Authentication
*/
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
// 去掉前缀 获取Token字符串
String token = tokenHeader.replace(JwtTokenUtil.TOKEN_PREFIX, "");
// 从Token中解密获取用户名
String username = JwtTokenUtil.getUsername(token);
// 从Token中解密获取用户角色
String role = JwtTokenUtil.getUserRole(token);
// 将[ROLE_XXX,ROLE_YYY]格式的角色字符串转换为数组
String[] roles = StringUtils.strip(role, "[]").split(", ");
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (String s : roles) {
authorities.add(new SimpleGrantedAuthority(s));
}
if (username != null) {
//这里向SpringSecurity声明用户角色,做相关操作放行
return new UsernamePasswordAuthenticationToken( username , null, null);
}
return null;
}
}
创建WebSecurityConfig配置类,对springsecurity进行相关配置
package com.wlj.config;
import com.wlj.util.JWTAuthenticationEntryPoint;
import com.wlj.util.JWTAuthenticationFilter;
import com.wlj.util.JWTAuthorizationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("accountDetailsServiceImpl")
private UserDetailsService userDetailsService;
/**
* 认证管理器配置方法
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
/**
* 安全配置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 跨域共享
http.cors()
.and()
// 跨域伪造请求限制无效
.csrf().disable()
.authorizeRequests()
// 访问需要拦截的url
.antMatchers("/user/login").permitAll()
.antMatchers("/api/login").permitAll()
.antMatchers("/user/update").permitAll( ) //根据角色访问
// 任何请求,访问的用户都需要经过认证
.anyRequest().authenticated()
// // 其余资源都可访问
// .anyRequest().permitAll()
.and()
// 添加JWT登录拦截器
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
// 添加JWT鉴权拦截器
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
.sessionManagement()
// 设置Session的创建策略为:Spring Security永不创建HttpSession 不使用HttpSession来获取SecurityContext
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 异常处理
.exceptionHandling()
// 匿名用户访问无权限资源时的异常 JWTAuthenticationEntryPoint
.authenticationEntryPoint(new JWTAuthenticationEntryPoint());
// http.formLogin();
http.headers().frameOptions().sameOrigin();
http.csrf().disable();
}
/**
* 跨域配置
*
* @return 基于URL的跨域配置信息
*/
@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 注册跨域配置
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
创建JWTAuthenticationEntryPoint
package com.wlj.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fawkes.core.base.api.ApiResponseBody;
import com.fawkes.core.enums.BizCodeMsgEnum;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("text/javascript;charset=utf-8");
// response.getWriter().print(JSONObject.toJSONString("您未登录,没有访问权限"));
response.setStatus(HttpServletResponse.SC_OK);
final ApiResponseBody error = ApiResponseBody.error(BizCodeMsgEnum.NO_TOKEN_AUTH);
response.getWriter().write(new ObjectMapper().writeValueAsString(error));
}
}
测试
访问/user/add
登录/api/login
注意 密码要和数据库的一致,加密方式也一样
密码可通过
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
//密码加密
String encode = bCryptPasswordEncoder.encode("123");
进行获取
由于使用的是调试工具测试的返回的token我也不知道在哪,可以通过代码输出进行获取
找到JWTAuthenticationFilter中的successfulAuthentication方法把token打印出来
System.out.println(JwtTokenUtil.TOKEN_PREFIX + token + "生成的token");
再次尝试登录拿到token
在/user/add接口Hander中添加token
再次访问/user/add,添加成功
上面代码是使用的默认的/api/login的登录接口
如果使用自定义的登录接口,其实更简单只是放行/user/login,依次验证用户名密码没有问题之后,和api/login生成的token逻辑一致即可,访问时携带token,JWTAuthorizationFilter会根据security配置进行拦截验证token