shiro整合springboot+jwt

同类博客:SpringSecurity整合springboot+jwt

目录

目录结构

依赖

 配置文件

代码介绍

SecurityProperties

AuthController

ShiroConfiguration

JwtAuthFilter

 DbShiroRealm

JwtShiroRealm

JwtToken

JwtTokenProvider

用于权限测试的接口类

 操作

github地址


 

目录结构

红框中是核心配置文件

shiro整合springboot+jwt_第1张图片

依赖



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.3.0.RELEASE
         
    
    com.siyang
    shiro-springboot-demo
    0.0.1-SNAPSHOT
    shiro-springboot-demo
    Demo project for Spring Boot

    
        1.8
    

    
        
            org.springframework.boot
            spring-boot-starter-data-jpa
        
        
            org.springframework.boot
            spring-boot-starter-web
        

        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
        
        
            org.apache.shiro
            shiro-spring-boot-web-starter
            1.4.1
        
        
        
            mysql
            mysql-connector-java
            runtime
        
        
        
            com.alibaba
            druid-spring-boot-starter
            1.1.10
        

        
        
            org.projectlombok
            lombok
            true
        
        
        
            com.alibaba
            fastjson
            1.2.54
        
        
        
            cn.hutool
            hutool-all
            5.0.6
        
        
        
            io.jsonwebtoken
            jjwt-api
            0.10.6
        
        
            io.jsonwebtoken
            jjwt-impl
            0.10.6
        
        
            io.jsonwebtoken
            jjwt-jackson
            0.10.6
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
            
                
                    org.junit.vintage
                    junit-vintage-engine
                
            
        
        
            junit
            junit
            test
        
        
            junit
            junit
            test
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

 配置文件

spring:
  datasource:
    druid:
      url: jdbc:mysql://localhost:3306/securitydemo?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false
      username: root
      password: root
      driver-class-name: com.mysql.jdbc.Driver
  jpa:
    show-sql: true
    hibernate:
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

#jwt
jwt:
  header: Authorization
  # 令牌前缀
  token-start-with: Bearer
  # 必须使用最少88位的Base64对该令牌进行编码
  base64-secret: ZmQ0ZGI5NjQ0MDQwY2I4MjMxY2Y3ZmI3MjdhN2ZmMjNhODViOTg1ZGE0NTBjMGM4NDA5NzYxMjdjOWMwYWRmZTBlZjlhNGY3ZTg4Y2U3YTE1ODVkZDU5Y2Y3OGYwZWE1NzUzNWQ2YjFjZDc0NGMxZWU2MmQ3MjY1NzJmNTE0MzI=
  # 令牌过期时间 此处单位/毫秒 ,默认4小时,可在此网站生成 https://www.convertworld.com/zh-hans/time/milliseconds.html
  token-validity-in-seconds: 14400000
  # 在线用户key
  online-key: online-token
  # 验证码
  code-key: code-key

代码介绍

SecurityProperties

 

jwt配置文件参数映射

/**
 * @author siyang
 * @create 2020-01-12 14:45
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "jwt")//从yml配置文件中获取配置的属性
public class SecurityProperties {

    /** Request Headers : Authorization */
    private String header;

    /** 令牌前缀,最后留个空格 Bearer */
    private String tokenStartWith;

    /** 必须使用最少88位的Base64对该令牌进行编码 */
    private String base64Secret;

    /** 令牌过期时间 此处单位/毫秒 */
    private Long tokenValidityInSeconds;

    /** 在线用户 key,根据 key 查询 redis 中在线用户的数据 */
    private String onlineKey;

    /** 验证码 key */
    private String codeKey;

    public String getTokenStartWith() {
        return tokenStartWith + " ";
    }

}

AuthController

登录认证接口

/**
 * @author siyang
 * @create 2020-05-29 11:26
 */
@RestController
@RequestMapping("/auth")
public class AuthController {
    @Autowired
    private SecurityProperties properties;
    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @PostMapping("/login")
    public ResponseEntity login(User user){
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(user.getUsername(), user.getPassword());
        // 登录验证
        subject.login(usernamePasswordToken);

        // 生成token
        String token = jwtTokenProvider.createToken(user.getUsername());
        // 将token放入redis,没写

        Map authInfo = new HashMap(1){{
            put("token", properties.getTokenStartWith() + token);
        }};

        return ResponseEntity.ok(authInfo);
    }

} 
  

ShiroConfiguration

shiro配置类,配置数据源,拦截路径,权限注解开启

@Configuration
public class ShiroConfiguration {

    /**
     * 设置过滤器
     * @param securityManager
     * @return
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, JwtTokenProvider jwtTokenProvider){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //配置安全管理
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map filters = shiroFilterFactoryBean.getFilters();
        filters.put("authcToken", new JwtAuthFilter(jwtTokenProvider));
//        filters.put("anyRole", createRolesFilter());
        shiroFilterFactoryBean.setFilters(filters);
        shiroFilterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition().getFilterChainMap());

        return shiroFilterFactoryBean;

    }

    /**
     * 配置拦截路径及相应过滤器
     * @return
     */
    @Bean
    protected ShiroFilterChainDefinition shiroFilterChainDefinition() {

        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();

        chainDefinition.addPathDefinition("/*.html", "anon");
        chainDefinition.addPathDefinition("/**/*.html", "anon");
        chainDefinition.addPathDefinition("/**/*.css", "anon");
        chainDefinition.addPathDefinition("/**/*.js", "anon");
        chainDefinition.addPathDefinition("/webSocket/**", "anon");
        chainDefinition.addPathDefinition("/swagger-ui.html", "anon");
        chainDefinition.addPathDefinition("/swagger-resources/**", "anon");
        chainDefinition.addPathDefinition("/webjars/**", "anon");
        chainDefinition.addPathDefinition("/*/api-docs", "anon");
        chainDefinition.addPathDefinition("/avatar/**", "anon");
        chainDefinition.addPathDefinition("/file/**", "anon");
        chainDefinition.addPathDefinition("/druid/**", "anon");
        chainDefinition.addPathDefinition("/avatar/**", "anon");
        //控制器路径
        chainDefinition.addPathDefinition("/", "anon");
        chainDefinition.addPathDefinition("/auth/login", "anon");
        chainDefinition.addPathDefinition("/auth/code", "anon");
        chainDefinition.addPathDefinition("/auth/logout", "authcToken[permissive]");

        chainDefinition.addPathDefinition("/**", "authcToken");  //对所有进行验证token
        return chainDefinition;
    }

    /**
     * 禁用session, 不保存用户登录状态。保证每次请求都重新认证。
     * 需要注意的是,如果用户代码里调用Subject.getSession()还是可以用session,如果要完全禁用,要配合下面的noSessionCreation的Filter来实现
     */
    @Bean
    protected SessionStorageEvaluator sessionStorageEvaluator(){
        DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        return sessionStorageEvaluator;
    }

    /**
     * 注册shiro的Filter,拦截请求
     */
    @Bean
    public FilterRegistrationBean filterRegistrationBean(SecurityManager securityManager, JwtTokenProvider jwtTokenProvider) throws Exception{
        FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
        filterRegistration.setFilter((Filter)shiroFilter(securityManager, jwtTokenProvider).getObject());
        filterRegistration.addInitParameter("targetFilterLifecycle", "true");
        filterRegistration.setAsyncSupported(true);
        filterRegistration.setEnabled(true);
        filterRegistration.setDispatcherTypes(DispatcherType.REQUEST);

        return filterRegistration;
    }
    /**
     * 初始化Authenticator 身份认证
     */
    @Bean
    public Authenticator authenticator() {
        ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
        //设置两个Realm,一个用于用户登录验证和访问权限获取;一个用于jwt token的认证
        authenticator.setRealms(Arrays.asList(dbShiroRealm(),jwtShiroRealm()));
        //设置多个realm认证策略,一个成功即跳过其它的
        authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
        return authenticator;
    }
    /**
     * 初始化authorizer 认证器 权限认证
     * @return
     */
    @Bean
    public Authorizer authorizer() {
        ModularRealmAuthorizer authorizer = new ModularRealmAuthorizer();//这里的
        authorizer.setRealms(Arrays.asList(jwtShiroRealm()));
        return authorizer;
    }

    /**
     * DbRealm,默认的密码校验算法为BCrypt
     * @return
     */
    @Bean("dbRealm")
    public Realm dbShiroRealm() {
        DbShiroRealm myShiroRealm = new DbShiroRealm();
        //将Realm的默认密码校验设置为BCrypt算法
        myShiroRealm.setCredentialsMatcher(new CredentialsMatcher() {
            @Override
            public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
                String password = new String(((UsernamePasswordToken) authenticationToken).getPassword());
                String hashed = (String) authenticationInfo.getCredentials();
                return BCrypt.checkpw(password,hashed);
            }
        });
        return myShiroRealm;
    }
    /**
     * jwtToken->Realm
     * 校验前后token是否相同,其实可以直接返回true。
     * 因为前面过滤器已经验证过token的完整性和正确性
     * @return
     */
    @Bean("jwtRealm")
    public Realm jwtShiroRealm() {
        JwtShiroRealm myShiroRealm = new JwtShiroRealm();
        myShiroRealm.setCredentialsMatcher(new CredentialsMatcher() {
            @Override
            public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
                JwtToken jwtToken1 = (JwtToken) authenticationToken;
                JwtToken jwtToken2 = (JwtToken)authenticationInfo.getCredentials();
                String token1 = jwtToken1.getToken();
                String token2 = jwtToken2.getToken();

                return token1.equals(token2);
            }
        });
        return myShiroRealm;
    }
    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
     * @return
     */
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator creator  = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

 

JwtAuthFilter

shiro过滤器

 

 

/**
 * @author siyang
 * @create 2020-01-12 20:40
 * jwt过滤器
 * 不能写@bean之类的直接,不能交给spring管理
 */
@Slf4j
public class JwtAuthFilter extends BasicHttpAuthenticationFilter {

    private JwtTokenProvider jwtTokenProvider;


    public JwtAuthFilter(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }


    /**
     *父类会在请求进入拦截器后调用该方法,返回true则继续,返回false则会调用onAccessDenied()。这里在不通过时,还调用了isPermissive()方法,我们后面解释。
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

        boolean allowed = false;
        try {
            //内部的方法会执行createToken
            allowed = executeLogin(request, response);
        } catch(IllegalStateException e){ //not found any token
            log.error("Not found any token");
        }catch (Exception e) {
            log.error("Error occurs when login", e);
        }
        return allowed || super.isPermissive(mappedValue);
    }

    /**
     *  这里重写了父类的方法,使用我们自己定义的Token类,提交给shiro。这个方法返回null的话会直接抛出异常,进入isAccessAllowed()的异常处理逻辑。
     * @param request
     * @param response
     * @return
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        //从request头中取出token
        String token = jwtTokenProvider.getToken((HttpServletRequest) request);
        //如果token不为空 并且token验证合格
        if(token!=null){
            // 返回的JWTtoken 会被JwtShiroRealm 进行解析
            return new JwtToken(token);
        }

        return null;
    }


    /**
     * 如果这个Filter在之前isAccessAllowed()方法中返回false,则会进入这个方法。我们这里直接返回错误的response
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {

        HttpServletResponse res = (HttpServletResponse)servletResponse;
        res.setHeader("Access-Control-Allow-Origin", "*");
        res.setStatus(HttpServletResponse.SC_OK);
        res.setCharacterEncoding("UTF-8");
        PrintWriter writer = res.getWriter();
        Map map= new HashMap<>();
        map.put("status", 401);
        LocalDateTime now = LocalDateTime.now();
        now.format(DateTimeFormatter.ISO_DATE_TIME);

        map.put("timestamp", now.toString());
        map.put("message", "身份认证失败");
        writer.write(JSON.toJSONString(map));
        writer.close();
        return false;
    }
}

 

 DbShiroRealm

登录验证数据源

/**
 * @author siyang
 * @create 2020-01-12 20:15
 * 只需要登录验证,所以只需要继承AuthenticatingRealm
 */
public class DbShiroRealm extends AuthenticatingRealm {

    @Autowired
    private UserService userService;

    /**
     * 限定这个Realm只支持UsernamePasswordToken
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }

    /**
     * 验证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken usernamePasswordToken =(UsernamePasswordToken) authenticationToken;
        String username = usernamePasswordToken.getUsername();
        User user = userService.getUserByUsername(username);
        if(user == null) {
            // 账号不存在
            throw new AuthenticationException();
        }
        return new SimpleAuthenticationInfo(user,user.getPassword(),"dbRealm");
    }
}

JwtShiroRealm

jwt认证、授权数据源

/**
 * @author siyang
 * @create 2020-01-14 18:13
 * 需要jwt验证和授权,所以需要继承AuthorizingRealm
 */
public class JwtShiroRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    @Autowired
    private JwtTokenProvider jwtTokenProvider;


    /**
     * 限定这个Realm只支持我们自定义的JWT Token
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }



    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        JwtToken primaryPrincipal = (JwtToken)principalCollection.getPrimaryPrincipal();
        String token = primaryPrincipal.getToken();
        String username = jwtTokenProvider.parseToken(token);
        User user = userService.getUserByUsername(username);
        Set roles = user.getRoles();
        Set roleSet = new HashSet<>();
        Set permissionSet = new HashSet<>();
        for (Role role : roles) {
            roleSet.add(role.getName());
            // 将角色下所有权限都存入permissions
            Set p = role.getPermissions();
            for (Permission permission : p) {
                permissionSet.add(permission.getPermissionValue());
            }
        }

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setRoles(roleSet);
        simpleAuthorizationInfo.setStringPermissions(permissionSet);
        return simpleAuthorizationInfo;

    }

    /**
     * token登录,每次请求都要判断路径
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        JwtToken token = (JwtToken) authenticationToken;
        // 如果解析jwt有问题会跳出
        String username = jwtTokenProvider.parseToken(token.getToken());
        User user = userService.getUserByUsername(username);
        if(user == null) {
            // 账号不存在
            throw new AuthenticationException();
        }
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(token,token,"jwtRealm");
        return simpleAuthenticationInfo;
    }
}

JwtToken

为UsernamePasswordToken的兄弟类,用于区分使用哪个数据源,这个是使用jwtToken,在数据源中support中指定

/**
 * @author siyang
 * @create 2020-01-14 17:48
 * 封装的JwtToken对象
 */
public class JwtToken implements HostAuthenticationToken {
    private String token;
    private String host;
    public JwtToken(String token) {
        this(token, null);
    }
    public JwtToken(String token, String host) {
        this.token = token;
        this.host = host;
    }
    public String getToken(){
        return this.token;
    }
    public String getHost() {
        return host;
    }
    @Override
    public Object getPrincipal() {
        return token;
    }
    @Override
    public Object getCredentials() {
        return token;
    }
    @Override
    public String toString(){
        return token + ':' + host;
    }
}

JwtTokenProvider

jwt提供工具类

/**
 * @author siyang
 * @create 2020-01-12 20:50
 * JWT-TOKEN 提供类
 */
@Slf4j
@Component
public class JwtTokenProvider implements InitializingBean {

    @Autowired
    private SecurityProperties properties;
    private static final String AUTHORITIES_KEY ="auth";
    private Key key;
    /**
     * 实例化对前会调用此方法,需要Spring的环境
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        byte[] decode = Decoders.BASE64.decode(properties.getBase64Secret());
        this.key= Keys.hmacShaKeyFor(decode);
    }

    /**
     * 创建token
     * @return
     */
    public String createToken(String username){

        long now = new Date().getTime();
        Date date = new Date(now + properties.getTokenValidityInSeconds());
        return Jwts.builder().setSubject(username).setExpiration(date).signWith(key, SignatureAlgorithm.HS512).compact();

    }

    /**
     * 验证token
     * @return
     */
    public boolean vaildateToken (String token){

        try {
            Jwts.parser().setSigningKey(key).parseClaimsJws(token);
            return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT signature.");
            e.printStackTrace();
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT token.");
            e.printStackTrace();
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT token.");
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            log.info("JWT token compact of handler are invalid.");
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 根据token 解析出username
     * @param token
     * @return
     */
    public String parseToken(String token){
        Claims body = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
        String username = body.getSubject();
        return username;

    }
    /**
     * 从request中获取token
     * @param request
     * @return
     */
    public String getToken(HttpServletRequest request){
        String header = request.getHeader(properties.getHeader());
        if(header != null && header.startsWith(properties.getTokenStartWith())){
            header=header.substring(properties.getTokenStartWith().length());
        }
        return header;
    }

}

 

用于权限测试的接口类

/**
 * @author siyang
 * @create 2020-05-29 12:00
 */
@RestController
@RequestMapping("/")
public class UserController {
    @Autowired
    UserService userService;

    @RequiresPermissions("read")
    @GetMapping("getAll")
    public ResponseEntity getAllUsers(){
        List all = userService.findAll();
        Map map = new HashMap<>();
        map.put("list",all);
        return ResponseEntity.ok(map);
    }
}

 

 操作

使用resource/下的sql文件创建数据,然后使用postman测试接口,使用/auth/login登录 (sql文件存在2个用户siyang/lisi,密码123456)

然后登录成功会返回token信息,将它放入header中,header名为Authorization 。然后用get方式访问/getAll 接口获得用户信息,通过切换2个用户产生的token 值,可以看到权限的管理功能。

github地址

https://github.com/ebb94f53au/shiro-springboot-demo

 

 

 

 

 

你可能感兴趣的:(java)