Spring Security整合JWT实现前后端分离认证和权限管理(超详细)
核心步骤创建项目配置pom.xml在配置文件中写入jwt相关配置,并创建JWT的配置类,使用@ConfigurationProperties(prefix = “jwt”)与配置文件关联起来创建自己的用户类创建自己的认证失败类创建自己的权限不足类创建自己的认证成功处理类创建自己的UserDetailsService创建JWT工具类创建自定义的Token过滤器创建自己的Spring Secrity配
核心步骤
- 创建项目
- 配置pom.xml
- 在配置文件中写入jwt相关配置,并创建JWT的配置类,使用@ConfigurationProperties(prefix = “jwt”)与配置文件关联起来
- 创建自己的用户类
- 创建自己的无凭证处理类
- 创建自己的认证失败类
- 创建自己的权限不足类
- 创建自己的认证成功处理类
- 创建自己的UserDetailsService
- 创建JWT工具类
- 创建自定义的Token过滤器
- 创建自己的Spring Secrity配置类(将之前的自定义的配置全部设置进去)
一、创建项目
默认创建
Spring Boot
项目即可
项目目录:
二、配置pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 在自定义的处理类里面需要使用 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- jwt 相关依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
三、配置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<? 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);
}
}
七、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<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);
}
九、自定义UserDeailsService
在这里我们要实现用户信息的查询,将查询到的信息返回给
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中的用户是否包含在我们的数据库中等等,如果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配置类
这个配置类里面我们要之前的自定义配置全部加进去,并且对路由什么的进行配置
/**
* @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
注意: 此代码并不完全正确,应该是缺少了一部分代码的,请对比观看。
更多推荐
所有评论(0)