核心步骤
- 创建项目
- 配置pom.xml
- 在配置文件中写入jwt相关配置,并创建JWT的配置类,使用@ConfigurationProperties(prefix = “jwt”)与配置文件关联起来
- 创建自己的用户类
- 创建自己的无凭证处理类
- 创建自己的认证失败类
- 创建自己的权限不足类
- 创建自己的认证成功处理类
- 创建自己的UserDetailsService
- 创建JWT工具类
- 创建自定义的Token过滤器
- 创建自己的Spring Secrity配置类(将之前的自定义的配置全部设置进去)
一、创建项目
默认创建Spring Boot项目
二、配置pom.xml引入依赖
org.springframework.boot
spring-boot-configuration-processor
true
com.alibaba
fastjson
1.2.73
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-tomcat
provided
org.springframework.boot
spring-boot-starter-test
test
io.jsonwebtoken
jjwt
0.9.0
三、配置JWT相关设置
配置这个主要是为了方便更改jwt相关的一些配置属性,比如加密的时候使用的盐值,token的过期时间等等,可以使用@ConfigurationProperties(prefix = "jwt")将配置文件与类联系起来,方便在开发过程中使用
- 配置文件
#请求头
jwt.header=Authorization
#盐值
jwt.base64-secret=meng
#过期时间
jwt.token-validity-in-seconds=14400000
- 对应实体类
@Data
@ToString
@Configuration
@ConfigurationProperties(prefix = "jwt") //与配置文件中的数据关联起来(这个注解会自动匹配jwt开头的配置)
public class JwtProperties {
/** Request Headers : Authorization */
private String header;
/** Base64对该令牌进行编码 */
private String base64Secret;
/** 令牌过期时间 此处单位/毫秒 */
private Long tokenValidityInSeconds;
}
四、创建自己的用户类
最好实现UserDetails接口,可以方面后面的使用,当然也可以不实现,但是在一些地方需要返回UserDeatils类型的数据,你得再自己做一次转换,很麻烦
注意: 在实现UserDetails接口后,会让你实现下面的一堆方法,你要看清每一个方法都是返回什么信息的,然后对它进行更改,因为你刚刚实现这些方法时,它返回的要么是null,要么是false,下面这个是我改过的。
@Data
public class JwtUser implements UserDetails { //实现UserDeails接口
//用户名
private String username;
//密码
private String password;
// 权限(角色)列表
Collection authorities;
public JwtUser(String stuId, String password, List grantedAuthorities) {
this.username = stuId;
this.password = password;
this.authorities = grantedAuthorities;
}
@Override
public Collection getAuthorities() {
return this.authorities;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
五、自定义无凭证处理类及认证失败处理类
- 无凭证处理类
当用户没有携带有效凭证时,就会转到这里来,当然,还需要在Spring Security的配置类中指定自定义的处理类才可以
/**
* 认证失败处理类
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
System.out.println("无凭证");
Result r = new Result();
r.code(ResultCode.UNAUTHORIZED).message("无凭证");
// 使用fastjson
String json = JSON.toJSONString(r);
// 指定响应格式是json
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(json);
}
}
- 自定义认证失败类
当用户输入错误的账号或者密码时,就会进入这个处理类,同样要在配置类中指明
/**
* 认证失败处理类
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
System.out.println("无凭证");
Result r = new Result();
r.code(ResultCode.UNAUTHORIZED).message("无凭证");
// 使用fastjson
String json = JSON.toJSONString(r);
// 指定响应格式是json
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(json);
}
}
六、创建自定义权限不足处理类
/**
* 自定义无权访问处理类
*/
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
Result r = new Result();
r.code(ResultCode.FORBIDDEN).message("权限不足");
String json = JSON.toJSONString(r);
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(json);
}
}
七、JWT工具类
@Component
public class JwtTokenUtil {
// 注入自己的jwt配置
@Resource
private JwtProperties jwtProperties;
static final String CLAIM_KEY_USERNAME = "sub";
static final String CLAIM_KEY_AUDIENCE = "audience";
static final String CLAIM_KEY_CREATED = "created";
private static final String AUDIENCE_UNKNOWN = "unknown";
private static final String AUDIENCE_WEB = "web";
private static final String AUDIENCE_MOBILE = "mobile";
private static final String AUDIENCE_TABLET = "tablet";
public String getUsernameFromToken(String token) {
String username;
try {
final Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
public Date getCreatedDateFromToken(String token) {
Date created;
try {
final Claims claims = getClaimsFromToken(token);
created = new Date((Long) claims.get(CLAIM_KEY_CREATED));
} catch (Exception e) {
created = null;
}
return created;
}
public Date getExpirationDateFromToken(String token) {
Date expiration;
try {
final Claims claims = getClaimsFromToken(token);
//得到token的有效期
expiration = claims.getExpiration();
} catch (Exception e) {
expiration = null;
}
return expiration;
}
public String getAudienceFromToken(String token) {
String audience;
try {
final Claims claims = getClaimsFromToken(token);
audience = (String) claims.get(CLAIM_KEY_AUDIENCE);
} catch (Exception e) {
audience = null;
}
return audience;
}
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(jwtProperties.getBase64Secret())
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
//设置过期时间
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + jwtProperties.getTokenValidityInSeconds());
// return new Date(30 * 24 * 60);
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
return (lastPasswordReset != null && created.before(lastPasswordReset));
}
// Device用户检测当前用户的设备,用不到的话可以删掉(使用这个需要添加相应的依赖)
// private String generateAudience(Device device) {
// String audience = AUDIENCE_UNKNOWN;
// if (device.isNormal()) {
// audience = AUDIENCE_WEB;
// } else if (device.isTablet()) {
// audience = AUDIENCE_TABLET;
// } else if (device.isMobile()) {
// audience = AUDIENCE_MOBILE;
// }
// return audience;
// }
private Boolean ignoreTokenExpiration(String token) {
String audience = getAudienceFromToken(token);
return (AUDIENCE_TABLET.equals(audience) || AUDIENCE_MOBILE.equals(audience));
}
public String generateToken(String username) {
Map claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, username);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 生成token(最关键)
* @param claims
* @return
*/
String generateToken(Map claims) {
return Jwts.builder()
.setClaims(claims) //设置声明信息(用户名等)
.setExpiration(generateExpirationDate()) //设置过期时间
.signWith(SignatureAlgorithm.HS512, jwtProperties.getBase64Secret()) //设置签名
.compact();
}
public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
final Date created = getCreatedDateFromToken(token);
return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
&& (!isTokenExpired(token) || ignoreTokenExpiration(token));
}
public String refreshToken(String token) {
String refreshedToken;
try {
final Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
//TODO,验证当前的token是否有效
public Boolean validateToken(String token, UserDetails userDetails) {
JwtUser user = (JwtUser) userDetails;
final String username = getUsernameFromToken(token);
final Date created = getCreatedDateFromToken(token);
return (username.equals(user.getUsername())&& !isTokenExpired(token));
}
}
八、自定义认证成功处理类(关键)
- 当用户认证成功之后,要在这里为用户生成token,并返回给用户,需要用到自定义的jwt工具类,也需要在配置类中配置
/**
* 自定义认证成功处理器
*/
@Component
public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Resource
private JwtTokenUtil jwtTokenUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//生成token
final String realToken = jwtTokenUtil.generateToken(authentication.getName());
HashMap map = new HashMap<>();
map.put("token", realToken);
Result r = new Result();
r.code(ResultCode.SUCCESS).message("登录成功").data(map);
//将生成的authentication放入容器中,生成安全的上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
String json = JSON.toJSONString(r);
httpServletResponse.setContentType("text/json;charset=utf-8");
httpServletResponse.getWriter().write(json);
}
九、自定义UserDeailsService
@Service
@Transactional
public class JwtUserDetailServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.eq("username",userName);
Admin admin = this.userMapper.selectOne(wrapper);
if (admin == null){
throw new UsernameNotFoundException("用户名不存在");
}
return admin;
}
}
十、创建自定义的Token过滤器
这个过滤器的主要作用是为了在用户登录并获取到发配的token之后,在带着token发送请求时,要检验token,判断它是否携带着token,token是否过期,token中的用户是否包含在的数据库中等等,如果token有效,则直接让Spring Security形成安全上下文,不再进行验证
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private UserDetailsService userDetailsService;
@Resource
private JwtTokenUtil jwtTokenUtil;
@Resource
private JwtProperties jwtProperties;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
如果在前端测试时出现跨域问题,到收藏的博客里面看一看
String requestUrl = httpServletRequest.getRequestURI();
String authToken = httpServletRequest.getHeader(jwtProperties.getHeader());
String stuId = jwtTokenUtil.getUsernameFromToken(authToken);
System.out.println("进入自定义过滤器");
System.out.println("自定义过滤器获得用户名为 "+stuId);
//当token中的username不为空时进行验证token是否是有效的token
if (stuId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
//token中username不为空,并且Context中的认证为空,进行token验证
//TODO,从数据库得到带有密码的完整user信息
UserDetails userDetails = this.userDetailsService.loadUserByUsername(stuId);
if (jwtTokenUtil.validateToken(authToken, userDetails)) { //如username不为空,并且能够在数据库中查到
/**
* UsernamePasswordAuthenticationToken继承AbstractAuthenticationToken实现Authentication
* 所以当在页面中输入用户名和密码之后首先会进入到UsernamePasswordAuthenticationToken验证(Authentication),
* 然后生成的Authentication会被交由AuthenticationManager来进行管理
* 而AuthenticationManager管理一系列的AuthenticationProvider,
* 而每一个Provider都会通UserDetailsService和UserDetail来返回一个
* 以UsernamePasswordAuthenticationToken实现的带用户名和密码以及权限的Authentication
*/
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
//将authentication放入SecurityContextHolder中
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
十一、Spring Security配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Resource
private JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Resource
private JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandler;
@Resource
private LoginFailureHandler loginFailureHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 自定义的Jwt Token过滤器
@Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationTokenFilter();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.formLogin()
//自定义认证成功处理器
.successHandler(jwtAuthenticationSuccessHandler)
// 自定义失败拦截器
.failureHandler(loginFailureHandler)
// 自定义登录拦截URI
.loginProcessingUrl("/login")
.and()
//token的验证方式不需要开启csrf的防护
.csrf().disable()
// 自定义认证失败类
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
// 自定义权限不足处理类
.accessDeniedHandler(jwtAccessDeniedHandler)
.and()
//设置无状态的连接,即不创建session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers("/login").permitAll()
//配置允许匿名访问的路径
.anyRequest().authenticated();
// 解决跨域问题(重要) 只有在前端请求接口时才发现需要这个
httpSecurity.cors().and().csrf().disable();
//配置自己的jwt验证过滤器
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
// disable page caching
httpSecurity.headers().cacheControl();
}
}
十二、控制器
@RestController
public class AuthController {
@RequestMapping("/get")
public Result get(){
HashMap map = new HashMap();
map.put("username","admin");
map.put("password","123456");
Result r = new Result();
r.code(ResultCode.SUCCESS).message("成功访问").data(map);
return r;
}
@PreAuthorize("hasAuthority('admin')")
@RequestMapping("/del")
public String del(){
return "删除成功";
}
}