springboot + spring security + jwt实现api权限控制
1、在pom.xml中添加security和jwt的相关依赖,并在启动类上添加注解@EnableWebSecurity
<!-- 权限相关依赖(security和jwt)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2、新建用户实体类,继承了userDetails的属性,用于用户登录的授权验证
package com.rrf.securityjwt.dto;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* @Author:aha
* @Description:
* @Date: 2018/8/9 15:51
* @Modified By:
*/
public class JwtUser implements UserDetails {
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
public JwtUser() {
}
/**
* 自定义值
* @param phone
* @param openId
*/
public JwtUser(String phone,String openId) {
username = phone;
password = openId;
}
/**
*
* @return 获取权限信息
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
/**
* 账号是否未过期,默认是false
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 账号是否未锁定,默认是false
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 账号凭证是否未过期,默认是false
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 默认也是false
* @return
*/
@Override
public boolean isEnabled() {
return true;
}
@Override
public String toString() {
return "JwtUser{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", authorities=" + authorities +
'}';
}
}
3、在业务逻辑层重写UserDetailsService的loadUserByUsername方法,按实际需求来写相对应的“验证规则”即登录成功的评判或标准。
package com.rrf.securityjwt.service.impl;
import com.rrf.securityjwt.dto.JwtUser;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
/**
* @Author:aha
* @Description:自定义登录验证规则
* @Date: 2018/8/9 16:00
* @Modified By:
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
/**
* 密码加密方式
* @return
*/
private BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 根据传进来的参数来确认凭证
* 可以自定义 例如:用户的手机号->openid 可以对应于(name,password)
* @param phone
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {
/**
* 根据参数获取凭证 可以对接数据库 redis等
* 例如 当phone="15512343256"时,openId为"1002222224998989"
* 验证时需要有密码规则即需要用户注册时将密码加密插入数据库
* 举例:
*/
String realPhone = "15512343256";
String openId = "";
if(realPhone.equals(phone)){
openId = bCryptPasswordEncoder().encode("1002222224998989");
}
return new JwtUser(realPhone,openId);
}
}
4、在控制器层写两个方法用于后面的测试
package com.rrf.securityjwt.controller;
import com.rrf.securityjwt.util.OkhttpUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
/**
* @Author:aha
* @Description:
* @Date: 2018/8/9 16:19
* @Modified By:
*/
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping("/getNews")
public String getNews() throws IOException {
String news = OkhttpUtil.get("https://www.apiopen.top/journalismApi",null);
return news;
}
@RequestMapping("/hello")
public String hello() throws IOException {
return "请求成功";
}
}
5、新建JWTLoginFilter类,该类继承自UsernamePasswordAuthenticationFilter,重写了其中的2个方法 attemptAuthentication 和successfulAuthentication ,当验证用户名密码正确后,生成一个token,并将token返回给客户端。
package com.rrf.securityjwt.filter;
import com.rrf.securityjwt.dto.JwtUser;
import com.rrf.securityjwt.util.JwtTokenUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
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.ArrayList;
/**
* 验证用户名密码正确后,生成一个token,并将token返回给客户端
* 该类继承自UsernamePasswordAuthenticationFilter,重写了其中的2个方法
* attemptAuthentication :接收并解析用户凭证。
* successfulAuthentication :用户成功登录后,这个方法会被调用,我们在这个方法里生成token。
* @author aha on 2018/8/6 17:47
*/
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTLoginFilter(AuthenticationManager authenticationManager){
this.authenticationManager = authenticationManager;
}
/**
*接收并解析用户凭证
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
//从输入流中获取登录信息
try{
System.out.println("尝试登录");
//手机号
String phone = request.getParameter("phone");
//用户openId
String openId = request.getParameter("openId");
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
phone,openId,new ArrayList<>()
)
);
}catch (Exception e){
throw new RuntimeException(e);
}
}
/**
* 用户成功登录后,这个方法会被调用,我们在这个方法里生成token
*/
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
String principal = ((JwtUser)authResult.getPrincipal()).getUsername();
String token = JwtTokenUtils.createToken(principal,false);
System.out.println("【登录成功,token->】"+JwtTokenUtils.TOKEN_PREFIX+token);
response.addHeader(JwtTokenUtils.TOKEN_HEADER,JwtTokenUtils.TOKEN_PREFIX+token);
}
/**
* 这是验证失败时候调用的方法
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.getWriter().write("authentication failed, reason: " + failed.getMessage());
}
}
6、新建JWTAuthenticationFilter类,该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。 如果校验通过,就认为这是一个取得授权的合法请求。
package com.rrf.securityjwt.filter;
import com.rrf.securityjwt.util.JwtTokenUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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;
/**
* @Author:aha
* @Description:
* token的校验
* 该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,
* 从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。
* 如果校验通过,就认为这是一个取得授权的合法请求
* @Date: 2018/8/9 16:26
* @Modified By:
*/
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
public JWTAuthenticationFilter(AuthenticationManager authenticationManager){
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
// 如果请求头中没有Authorization信息则直接放行了
if(header == null || !header.startsWith(JwtTokenUtils.TOKEN_PREFIX)){
chain.doFilter(request,response);
return;
}
// 如果请求头中有token,则进行解析,并且设置认证信息
UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(header);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
chain.doFilter(request,response);
}
/**
*这里从token中获取用户信息并新建一个token
*/
private UsernamePasswordAuthenticationToken getAuthentication(String header) {
String token = header.replace(JwtTokenUtils.TOKEN_PREFIX, "");
String principal = JwtTokenUtils.getUsername(token);
if (principal != null) {
return new UsernamePasswordAuthenticationToken(principal, null, new ArrayList<>());
}
return null;
}
}
7、新建WebSecurityConfig类配置springsecurity,通过SpringSecurity的配置,将JWTLoginFilter,JWTAuthenticationFilter组合在一起。这里配置了,除hello/hello的get方法的请求不需要权限验证以外其余都需要验证才可以访问。
package com.rrf.securityjwt.security;
import com.rrf.securityjwt.filter.JWTAuthenticationFilter;
import com.rrf.securityjwt.filter.JWTLoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* @Author:aha
* @Description:
* SpringSecurity的配置
* 通过SpringSecurity的配置,将JWTLoginFilter,JWTAuthenticationFilter组合在一起
* @Date: 2018/8/9 16:43
* @Modified By:
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.GET,"/hello/hello").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.logout()
.and()
.addFilter(new JWTLoginFilter(authenticationManager()))
.addFilter(new JWTAuthenticationFilter(authenticationManager()));
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
jwttokenutil工具类
package com.rrf.securityjwt.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
/**
* @Author:aha
* @Description:jwt工具类
* @Date: 2018/8/9 16:30
* @Modified By:
*/
public class JwtTokenUtils {
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
private static final String SECRET = "jwtsecretdemo";
/**
* 签发者
*/
private static final String ISS = "LLy";
/**
* 过期时间是3600秒,既是1个小时
*/
private static final long EXPIRATION = 3600L;
/**
* 选择了记住我之后的过期时间为7天
*/
private static final long EXPIRATION_REMEMBER = 604800L;
/**
* 创建token
* @param username
* @param isRememberMe
* @return
*/
public static String createToken(String username, boolean isRememberMe) {
long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
String token = Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
.setIssuer(ISS)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.compact();
return token;
}
/**
* 从token中获取用户名
* @param token
* @return
*/
public static String getUsername(String token){
return getTokenBody(token).getSubject();
}
/**
* 判断是否已过期
* @param token
* @return
*/
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();
}
}
接下来用postman请求测试一下:
(1)当没有带token时hello/hello也是可以访问的
当没有带token时,访问hello/getNews,被拦截到登录页面
此时访问登录接口即/login,这是security自己默认的登录接口(**这里是post请求方式),如果需要自定义登录页面的话可以在WebSecurityConfig配置类中修改 .formLogin() .loginPage(url)
当验证成功后,返回的请求头中会带Authorization,对应的值就是认证过后的jwttoken,固定格式是以Bearer 开头的
接下来我们带上token访问需要认证的hello/getNews接口
访问成功!!!
最后的最后,一切要以实际需求来自定义登录鉴权的标准。。。
上一篇: Lock(四) — 可重入锁(ReentrantLock)源码解析
下一篇: 多线程基础知识点
推荐阅读
-
SpringBoot+JWT实现登录权限控制(代码)
-
SpringBoot集成Spring security JWT实现接口权限认证
-
Springboot + Spring Security 前后端分离权限控制
-
jwt,spring security ,feign,zuul,eureka 前后端分离 整合 实现 简单 权限管理系统 与 用户认证的实现
-
Springboot+Spring Security实现前后端分离登录认证及权限控制的示例代码
-
Spring security实现登陆和权限角色控制
-
《Spring Security3》第五章第二部分翻译下(实现授权精确控制的方法——页面级权限)
-
《Spring Security3》第五章第二部分翻译上(实现授权精确控制的方法——页面级权限)
-
springboot + spring security + jwt实现api权限控制
-
SpringBoot中使用Spring Security实现权限控制