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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值