springBoot+springSecurity+jwt实现权限管理以及token管理
前言
首先这里是关于springSecurity+jwt的一个权限管理,因为目前大多数项目都普遍会用到权限这方面的功能,所以自己也学着写写,不由自主的就会想到springSecurity或者是shiro来实现不同角色拥有不同权限,来访问相应的资源。两者各有优缺点。
优点:
1:Spring Security基于Spring开发,项目中如果使用Spring作为基础,配合Spring Security做权限更加方便,而Shiro需要和Spring进行整合开发
2:Spring Security功能比Shiro更加丰富些,例如安全防护
3:Spring Security社区资源比Shiro丰富
缺点:
1:Shiro的配置和使用比较简单,Spring Security上手复杂
2:Shiro依赖性低,不需要任何框架和容器,可以独立运行,而Spring Security依赖于Spring容器
一、项目配置
1.依赖注入
这里引入了jwt、数据源、Json、redis、jpa以及连接池的相关依赖。
<dependencies>
<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>
<!--jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.20</version>
</dependency>
<!--spring-data-jpa-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.61</version>
</dependency>
<!--durid数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.20</version>
</dependency>
2.yml配置文件
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost/test?characterEncoding=utf-8
type: com.alibaba.druid.pool.DruidDataSource
password: ******
username: root
jpa:
database: mysql
show-sql: true
hibernate:
ddl-auto: update
redis:
host: localhost
port: 6379
password: ******
database: 0
这里虽然配置了redis,但是项目中并没有把token与redis进行关联。
二、实体类
这里就没有整合mybatis进行数据操作,而是使用的Jpa。项目运行后自动建表和字段名。
package com.security.demo.entity;
import javax.persistence.*;
/**
* 用户实体类
* @author zwj
* @date 2020/08/19
* @version 1.0
*/
@Entity
@Table(name = "tb_user")
public class User {
@Id
@GeneratedValue
@Column(name = "id")
private Integer id;
@Column(name = "username")
private String username;
@Column(name = "password")
private String password;
@Column(name = "role")
private String role;
public Integer getId() { return id; }
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}
三、Repository
创建
UserRepository
继承JpaRepository<User,Integer>
类,写一个根据用户名查找用户的方法。
就不需要和之前用mybatis要去写接口、xml文件等等。这个用起来很方便。
package com.security.demo.repository;
import com.security.demo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* 用于查询数据库
* @author zwj
* @date 2020/08/19
* @version 1.0
*/
public interface UserRepository extends JpaRepository<User,Integer> {
/**
* 通过username查找user信息
* @param username
* @return
*/
User findByUsername(String username);
}
四、jwt工具类
主要是对redis和jwt的操作,使用redis实现登录、登出动态管理。(redis的部分暂时没有实现)
/**
* Token工具类
* @author zengwenjie
* @date 2020/8/19 23:5
* @version 1.0
*/
@Component
public class JwtTokenUtil {
public static final String TOKEN_HEADER="Authorization";
public static final String TOKEN_PREFIX="Bearer ";
/**
* 秘钥
*/
public static final String SECRET="lovejly";
public static final String ISS="MANAGER_ZWJ";
/**
* 过期时间是3600秒,既是1个小时
*/
private static final long EXPIRATION = 60*60*1000;
public static StringRedisTemplate redisTemplate;
/**
* 创建Token
* @return token
*/
public static String createToken(UserDetails userDetails){
Map<String,Object> claims=new HashMap<>();
Collection<? extends GrantedAuthority> authorities=userDetails.getAuthorities();
String role="";
for (GrantedAuthority authority:authorities){
role=authority.getAuthority();
}
claims.put("role",role);
return doGenerateToken(claims,userDetails.getUsername());
}
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
public static String doGenerateToken(Map<String,Object> claims,String subject){
Date expiration=new Date(System.currentTimeMillis()+EXPIRATION);
return Jwts.builder()
//签发时间
.setIssuedAt(new Date())
//签发者
.setIssuer(ISS)
//声明
.setClaims(claims)
//过期时间
.setExpiration(expiration)
//面向用户
.setSubject(subject)
.signWith(SignatureAlgorithm.HS256,SECRET)
.compact();
}
/**
* 获取用户名
* @param token
* @return username
*/
public static String getUsername(String token){
return getTokenBody(token).getSubject();
}
/**
* 获取用户角色
* @param token
* @return role
*/
public static String getRole(String token){
return getTokenBody(token).get("role").toString();
}
/**
* 解析token
* @param token
* @return
*/
public static Claims getTokenBody(String token){
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}
/**
* token是否过期
* @param token
* @return true or false
*/
public static boolean isExpiration(String token){
return getTokenBody(token).getExpiration().before(new Date());
}
/**
* 刷新token
* @param token
* @return token
*/
public static String refreshToken(String token){
String refreshToken;
try {
Claims claims=getTokenBody(token);
claims.put("create",new Date());
refreshToken=doGenerateToken(claims,claims.getSubject());
}catch (Exception e){
refreshToken=null;
}
return refreshToken;
}
/**
* 验证Token
* @return true or false
*/
public static boolean validateToken(String token,String username){
String subject=getUsername(token);
return (subject.equals(username)
&&!isExpiration(token));
}
}
五、业务逻辑类
使用springSecurity需要实现UserDetailsService
接口供权限框架调用,重写loadUserByUsername()
方法,要调用的方法也就是上面repository中定义的方法。
/**
* 账号密码的验证
* @author zengwenjie
* @date 2020/8/19 23:5
* @version 1.0
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository){
this.userRepository=userRepository;
}
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user=userRepository.findByUsername(s);
if (user==null){
throw new UsernameNotFoundException(s);
}
return new JwtUser(user);
//return new org.springframework.security.core.userdetails.User(user.getUsername(),user.getPassword(),emptyList());
}
}
由于该接口方法需要返回一个UserDetails
类型的接口,可以去实现UserDetails接口,也可以不用实现。org.springframework.security.core.userdetails.User
该封装类已经实现了UserDetails接口,可以直接调用,也可以自定义类去实现接口。这里就用的是自定义类。
JwtUser
/**
* 提供用户信息
* @author zwj
* @date 2020/08/19
* @version 1.0
*/
public class JwtUser implements UserDetails {
private Integer id;
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
/**
* 使用user创建jwtUser的构造器
* @param user
*/
public JwtUser(User user) {
id = user.getId();
username = user.getUsername();
password = user.getPassword();
authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole()));
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return 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;
}
}
六、过滤器
使用JwtLoginFilter
去进行用户账号的验证,使用JWTAuthorizationFilter
去进行用户权限的验证。
创建
JwtLoginFilter
类继承UsernamePasswordAuthenticationFilter
类,
重写了其中的2个方法:attemptAuthentication :接收并解析用户凭证。
successfulAuthentication :用户成功登录后,这个方法会被调用,我们在这个方法里生成token,并调用authenticationManager.authenticate()让springSecurity去验证账号的合法性。
/**
* 启动登录认证流程过滤器
* @author zwj
* @date 2020/08/19
* @version 1.0
*/
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
/**
* 构造方法注入authenticationManager
* @param authenticationManager
*/
public JwtLoginFilter(AuthenticationManager authenticationManager){
this.authenticationManager=authenticationManager;
super.setFilterProcessesUrl("/auth/login");
}
// @Override
// public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
// super.doFilter(req, res, chain);
// }
/**
* 接收并解析用户凭证
* @param request
* @param response
* @return
* @throws AuthenticationException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
/*
从流中读取用户信息,接收并解析用户凭证
*/
User user = new ObjectMapper()
.readValue(request.getInputStream(), User.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
user.getUsername(),
user.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 用户成功登录后,这个方法会被调用,我们在这个方法里生成token
* @param request
* @param response
* @param chain
* @param authResult
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
JwtUser jwtUser=(JwtUser) authResult.getPrincipal();
String token= JwtTokenUtil.createToken(jwtUser);
response.setHeader("token",JwtTokenUtil.TOKEN_PREFIX+token);
}
/**
* 验证失败后调用
* @param request
* @param response
* @param failed
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.getWriter().write("authentication failed, reason: " + failed.getMessage());
}
}
授权验证
用户一旦登录成功后,在response中拿到token,后续的请求都会带着这个token,服务端会验证token的合法性。
创建JwtAuthenticationFilter
类,该类继承自BasicAuthenticationFilter
,在doFilterInternal方法中,从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。如果校验通过,就认为这是一个取得授权的合法请求。
/**
* 验证成功当然就是进行鉴权了
* 登录成功之后走此类进行鉴权操作
* @author jie
* @date 2020/8/21 0:02
* @version 1.0
*/
@Component
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
@Autowired
public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String tokenHead=request.getHeader(JwtTokenUtil.TOKEN_HEADER);
if (tokenHead==null||!tokenHead.startsWith(JwtTokenUtil.TOKEN_PREFIX)){
chain.doFilter(request,response);
return;
}
// 如果请求头中有token,则进行解析,并且设置认证信息
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHead));
super.doFilterInternal(request, response, chain);
}
/**
* 这里从token中获取用户信息并新建一个token
* @param tokenHead
* @return new token
*/
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHead){
String token=tokenHead.replace(JwtTokenUtil.TOKEN_PREFIX,"");
String username=JwtTokenUtil.getUsername(token);
String role=JwtTokenUtil.getRole(token);
if (username!=null){
return new UsernamePasswordAuthenticationToken(username,null,
Collections.singleton(new SimpleGrantedAuthority(role)));
}
return null;
}
}
七、security配置类
SecurityConfig
类继承WebSecurityConfigurerAdapter
,
开启注解@EnableWebSecurity, @EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity的作用
1.加载了WebSecurityConfiguration配置类, 配置安全认证策略。2.加载了AuthenticationConfiguration, 配置了认证信息。
/**
* @author zwj
* @date 2020/08/19
* @version 1.0
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 实现UserDetailsService接口,用来做登陆验证
*/
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
@Autowired
private CustomAccessDeniedHandler customAccessDeniedHandler;
/**
* 加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//认证用户时用户信息加载配置,注入自定义 UserDetailsService
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.csrf()
.disable()
//匿名访问,没有权限的处理类
.exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint)
//登录后,访问没有权限处理类
.accessDeniedHandler(customAccessDeniedHandler)
.and()
.addFilter(new JwtLoginFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
// 基于token,不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//设置权限
.authorizeRequests()
//测试资源,验证了用户才可访问
.antMatchers("/task/**").authenticated()
//需要角色为ADMIN才能访问该路径下的资源
.antMatchers("/task/**").hasRole("ADMIN")
.anyRequest().permitAll();
}
}
这里也可以配置不同的资源路径由不同权限的角色用户进行访问。
比如:
1./task/** -->ROLE_ADMIN
2./test/** -->ROLE_USER
八、controller
UserController
这里写了一个注册的的接口,创建的角色默认为ROLE_ADMIN
/**
* @author zwj
* @date 2020/08/19
* @version 1.0
*/
@RestController
@RequestMapping("/auth")
public class UserController {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/register")
public JSONObject registerUser(User user){
user.setUsername(user.getUsername());
user.setPassword(passwordEncoder.encode(user.getPassword()));
user.setRole("ROLE_ADMIN");
userRepository.save(user);
JSONObject jsonObject=new JSONObject();
jsonObject.put("user",user);
return jsonObject;
}
}
TaskController
对于/task/**路径的资源,角色必须为ROLE_ADMIN
才可访问。
/**
* @author zwj
* @date 2020/08/19
* @version 1.0
*/
@RestController
@RequestMapping("/task")
public class TaskController{
@GetMapping("/zwj")
public JSONObject listTasks(){
System.out.println("1111");
JSONObject jsonObject=new JSONObject();
jsonObject.put("zwj","lovejly");
return jsonObject;
}
@PostMapping
public String newTasks(){
return "创建了一个新任务";
}
@PutMapping("/{taskId}")
public String updateTasks(@PathVariable("taskId") Integer id){
return "更新了一下id为:"+id+"的任务";
}
@DeleteMapping("/{taskId}")
public String deleteTasks(@PathVariable("taskId") Integer id){
return "删除了id为:"+id+"的任务";
}
}
九、测试结果
首先进行用户的注册,这里就用postman工具。
注册成功后,我们直接访问/task/zwj,这里显示需要授权才可访问。
然后访问/auth/login进行登录,生成token
然后带着token再去访问/task/**下的资源
总结:这里没有用到redis对token进行管理,是因为生成token的时候,把用户名、角色、过期时间等当做生成token的参数了,所以每次解析token都可以解析出该token的过期时间,所以就不需要redis来存取token了。
完成了这样的一个demo,还是发现需要真正地去弄懂框架的结构,才能知道需要用什么接口,如何配置等等,这里也在网上看了不少资源,借鉴了不少写的不错的博客,自己也获益颇多。
十、参考文献
https://blog.csdn.net/ech13an/article/details/80779973
https://blog.csdn.net/sxdtzhaoxinguo/article/details/77965226