- 创建项目
- 配置pom.xml
- 在配置文件中写入jwt相关配置,并创建JWT的配置类,使用@ConfigurationProperties(prefix = “jwt”)与配置文件关联起来
- 创建自己的用户类
- 创建自己的无凭证处理类
- 创建自己的认证失败类
- 创建自己的权限不足类
- 创建自己的认证成功处理类
- 创建自己的UserDetailsService
- 创建JWT工具类
- 创建自定义的Token过滤器
- 创建自己的Spring Secrity配置类(将之前的自定义的配置全部设置进去)
默认创建
Spring Boot
项目即可
pom.xml
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.73version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-tomcatartifactId>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.0version>
dependency>
配置这个主要是为了方便更改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<? extends GrantedAuthority> authorities;
public JwtUser(String stuId, String password, List<GrantedAuthority> grantedAuthorities) {
this.username = stuId;
this.password = password;
this.authorities = grantedAuthorities;
}
@Override
public Collection<? extends GrantedAuthority> 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);
}
}
当用户输入错误的账号或者密码时,就会进入这个处理类,同样要在配置类中指明(这个类上面的图片中没有,因为我第一个版本没写,这个类应当放到security包下)
/**
* 自定义认证失败处理类
*/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
returnFailure(httpServletResponse);
}
public void returnFailure(HttpServletResponse response) throws IOException{
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);
}
}
这个我是直接复制的别人的,经过自己稍微的修改用起来的,核心的功能就那么几个,只要能与自己的功能对应上就可以,比如我这个刚拿过来的时候它的好多配置都是写死的,而我的一些配置都在配置文件中,那就只需要找到相应位置,改成自己的就行
@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<String, Object> 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<String, Object> 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<String,Object> 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);
}
在这里我们要实现用户信息的查询,将查询到的信息返回给
Spring Security
,让它进行信息的对比,在比对过后会跳转到相应的处理类
这里应该是要到数据库中去查询,我这里暂时写成固定的了
@Service
public class JwtUserDetailServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//暂时写成固定的
if(!s.equals("admin")) return null;//用户不是admin,报错
System.out.println("查询"+s);
return new JwtUser("admin","$2a$10$WtN/BQbwY8dI0me.JsLxP.yyGePyTMg3bi3GZeRogowB4ZuoL1zrK", AuthorityUtils.commaSeparatedStringToAuthorityList("user"));
}
}
通常都是通过
AuthorityUtils.commaSeparatedStringToAuthorityList(“”)
来创建authorities
集合对象的。参数是一个字符串,多个权限使用逗号分隔。
- 角色授权:授权代码需要加
ROLE_
前缀,controller上使用时不要加前缀- 权限授权:设置和使用时,名称保持一致即可
@Service
@Transactional
public class JwtUserDetailServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
QueryWrapper<Admin> 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有效,则直接让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);
}
}
这个配置类里面我们要之前的自定义配置全部加进去,并且对路由什么的进行配置
/**
* @ClassName: WebSecurityConfig
* @Description: TODO Spring Security 配置类
* @Author 孟祥龙
* @Date: 2021/4/13 8:52
* @Version 1.0
*/
@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();
}
}
关于
@EnableGlobalMethodSecurity
:https://blog.csdn.net/chihaihai/article/details/104678864
注意:Spring Security 默认的加密方式就是BCrypt,如果想要详细了解请自行百度
@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 "删除成功";
}
}
源码地址:https://github.com/mengxianglong123/SpringSecurity-Demo/tree/master
注意: 此代码并不完全正确,应该是缺少了一部分代码的,请对比观看。