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、在application.yml中配置mysql及jwt等

server:
  port: 9696
  servlet:
    context-path: /demo
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/security-jwt?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: 12345
  jpa:
    show-sql: true

mybatis-plus:
  mapper-locations: classpath:/mapper/*Mapper.xml
  type-aliases-package: com.lan.demo.entity
  configuration:
    map-underscore-to-camel-case: true

logging:
  level:
    com.lan.demo.mapper: debug

jwt:
  tokenHeader: Authorization
  tokenPrefix: Bearer
  secret: lanjwt
  expiration: 3600
  rememberExpiration: 604800

3、新建用户实体类,实现userDetails的方法,用于用户登录的授权验证

/**
 * @author: Lan
 * @date: 2019/4/9 11:28
 * @description:登录成功返回
 */
@Data
public class LoginSuccessVO {

    /**
     * 用户编号
     */
    private String userId;

    /**
     * 用户手机号码
     */
    private String userPhone;

    /**
     * 角色信息
     */
    private List<String> roles;

    /**
     * 用户名
     */
    private String name;
}

/**
 * @author: Lan
 * @date: 2019/4/8 14:07
 * @description:用于校验的用户对象
 */
@Data
public class UserDTO extends LoginSuccessVO implements UserDetails {

    /**
     * 是否记住密码
     */
    private Boolean remember;

    /**
     * 用户名
     */
    private String userName;

    /**
     * 用户密码
     */
    private String userPassword;


    /**
     * 获取权限信息
     *
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> grantedAuthorities =
                getRoles().stream().map(roleName -> new SimpleGrantedAuthority("ROLE_" + roleName)).collect(Collectors.toList());
        return grantedAuthorities;
    }

    @Override
    public String getPassword() {
        return userPassword;
    }

    @Override
    public String getUsername() {
        return userName;
    }

    /**
     * 账户是否未过期
     *
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 账户是否未锁定
     *
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 账户凭证是否未过期
     *
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

​

4、在业务逻辑层重写UserDetailsService的loadUserByUsername方法,按实际需求来写相对应的“验证规则”即登录成功的评判或标准,返回实现UserDetails的userDTO对象。



/**
 * @author: Lan
 * @date: 2019/4/8 15:24
 * @description:
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String json) throws UsernameNotFoundException {
        LoginUser loginUser = JSON.parseObject(json, LoginUser.class);
        UserDTO userDTO = userMapper.getUserDTO(loginUser.getUserPhone());
        if (userDTO == null) {
            return null;
        }
        userDTO.setRemember(loginUser.getRemember());
        userDTO.setName(userDTO.getUsername());
        userDTO.setUserName(json);
        return userDTO;
    }
}

​

5、新建JWTLoginFilter类,该类继承自UsernamePasswordAuthenticationFilter,重写了其中的2个方法 attemptAuthentication 和successfulAuthentication ,当登录成功后,生成一个token,并将token返回给客户端。

/**
 * @author: Lan
 * @date: 2019/4/8 15:27
 * @description:处理登录请求
 */
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private UserMapper userMapper;

    private AuthenticationManager authenticationManager;

    public JwtLoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    /**
     * 请求登录
     *
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            LoginForm loginForm = new ObjectMapper().readValue(request.getInputStream(), LoginForm.class);
            checkLoginForm(loginForm, response);
            LoginUser loginUser = new LoginUser();
            BeanUtils.copyProperties(loginForm, loginUser);
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(JSON.toJSONString(loginUser), loginForm.getUserPassword(), new ArrayList<>()));
        } catch (IOException e) {
            ResponseUtil.write(response, ResultUtil.error("数据读取错误"));
        }
        return null;
    }

    /**
     * 登录成功后
     *
     * @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 {
        UserDTO userDTO = (UserDTO) authResult.getPrincipal();
        if (jwtTokenUtil == null) {
            jwtTokenUtil = (JwtTokenUtil) SpringUtils.getBean("jwtTokenUtil");
        }
        User user = new User();
        user.setUserId(userDTO.getUserId());
        user.setUserLastLoginTime(TimeUtil.nowTimeStamp());
        if (userMapper == null) {
            userMapper = (UserMapper) SpringUtils.getBean("userMapper");
        }
        //更新登最近一次录时间
        userMapper.updateById(user);
        String token = jwtTokenUtil.createToken(userDTO);
        //将token放置请求头返回
        response.addHeader(jwtTokenUtil.getTokenHeader(), jwtTokenUtil.getTokenPrefix() + token);
        LoginSuccessVO loginSuccessVO = new LoginSuccessVO();
        BeanUtils.copyProperties(userDTO, loginSuccessVO);
        ResponseUtil.write(response, ResultUtil.success(loginSuccessVO));
    }

    /**
     * 登录失败
     *
     * @param request
     * @param response
     * @param failed
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        ResponseUtil.write(response, ResultUtil.error(failed.getMessage()));
    }

    /**
     * 校验参数
     *
     * @param loginForm
     */
    private void checkLoginForm(LoginForm loginForm, HttpServletResponse response) {
        if (BlankUtil.isBlank(loginForm.getUserPhone())) {
            ResponseUtil.write(response, ResultUtil.error("手机号不能为空"));
            return;
        }
        if (BlankUtil.isBlank(loginForm.getUserPassword())) {
            ResponseUtil.write(response, ResultUtil.error("密码不能为空"));
            return;
        }
    }
}

6、新建JWTAuthenticationFilter类,该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。 如果校验通过,就认为这是一个取得授权的合法请求。在这也可以实现刷新token的操作(可以看情况加上去)。

/**
 * @author: Lan
 * @date: 2019/4/8 15:28
 * @description:token 校验过滤器
 */
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (jwtTokenUtil == null) {
            jwtTokenUtil = (JwtTokenUtil) SpringUtils.getBean("jwtTokenUtil");
        }
        String header = request.getHeader(jwtTokenUtil.getTokenHeader());
        //当token为空或格式错误时 直接放行
        if (header == null || !header.startsWith(jwtTokenUtil.getTokenPrefix())) {
            chain.doFilter(request, response);
            return;
        }

        UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(header);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        chain.doFilter(request, response);
    }

    /**
     * 这里从token中获取用户信息并新建一个token
     */
    private UsernamePasswordAuthenticationToken getAuthentication(String header) {

        String token = header.replace(jwtTokenUtil.getTokenPrefix(), "");
        String principal = jwtTokenUtil.getUserName(token);
        if (principal != null) {
            UserDTO userDTO = jwtTokenUtil.getUserDTO(token);
            return new UsernamePasswordAuthenticationToken(principal, null, userDTO.getAuthorities());
        }
        return null;
    }
}

7、要实现通过角色动态控制url权限


/**
 * @author: Lan
 * @date: 2019/4/9 14:56
 * @description:判断是否具有权限访问当前资源
 */
@Component("rbacauthorityservice")
public class RbacAuthorityService {

    @Autowired
    private PermissionMapper permissionMapper;

    /**
     * 判断是否有权限
     *
     * @param request
     * @param authentication
     * @return
     */
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        Collection<ConfigAttribute> collection = getAttributes(request);
        if (authentication.getPrincipal().equals("anonymousUser")) {
            return false;
        }

        if (null == collection || collection.size() <= 0) {
            return true;
        }

        ConfigAttribute configAttribute;
        String needRole;
        for (Iterator<ConfigAttribute> iterator = collection.iterator(); iterator.hasNext(); ) {
            configAttribute = iterator.next();
            needRole = configAttribute.getAttribute();
            for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
                if (needRole.trim().equals(grantedAuthority.getAuthority())) {
                    return true;
                }
            }
        }
        throw new AccessDeniedException("权限不足");
    }

    /**
     * 判定用户请求的url是否在权限表中,如果在权限表中,则返回decide方法,
     * 用来判定用户是否有权限,如果不在权限表中则放行
     *
     * @param request
     * @return
     * @throws IllegalArgumentException
     */
    public Collection<ConfigAttribute> getAttributes(HttpServletRequest request) throws IllegalArgumentException {
        HashMap<String, Collection<ConfigAttribute>> map = PermissionMap.map;
        if (map == null) {
            map = loadResourceDefine(map);
        }
        for (Map.Entry<String, Collection<ConfigAttribute>> entry : map.entrySet()) {
            String url = entry.getKey();
            if (new AntPathRequestMatcher(url).matches(request)) {
                return map.get(url);
            }
        }
        return null;
    }

    /**
     * 加载权限表中所有权限
     */
    private HashMap<String, Collection<ConfigAttribute>> loadResourceDefine(HashMap<String, Collection<ConfigAttribute>> map) {
        map = new HashMap<>();
        List<PermissionDto> all = permissionMapper.findAll();
        for (PermissionDto permissionDto : all) {
            List<ConfigAttribute> configAttributeList = permissionDto.getRoleNames().stream().map(roleName -> {
                ConfigAttribute configAttribute = new SecurityConfig("ROLE_" + roleName.toUpperCase());
                return configAttribute;
            }).collect(Collectors.toList());
            map.put(permissionDto.getPermissionUrl(), configAttributeList);
        }
        PermissionMap.map = map;
        return map;
    }
}

8、新建WebSecurityConfig类配置springsecurity,通过SpringSecurity的配置,将JWTLoginFilter,JWTAuthenticationFilter组合在一起等


/**
 * @author: Lan
 * @date: 2019/4/8 15:22
 * @description:security配置
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable().authorizeRequests()
                .antMatchers("/user/hello").permitAll()
                .anyRequest().authenticated()
                .anyRequest()
                .access("@rbacauthorityservice.hasPermission(request,authentication)")
                .and()
                .addFilter(new JwtLoginFilter(authenticationManager()))
                .addFilter(new JwtAuthenticationFilter(authenticationManager()))
                .exceptionHandling().accessDeniedHandler(new RestAuthenticationAccessDeniedHandler())
                .authenticationEntryPoint(new AuthEntryPoint());
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
}

9.注销登录,将资源数据源赋值为null,建议在登录的时候加入redis存取token,注销时可以将对应token清除,token达到无效的作用,在校验token时除了校验token合法性还需与redis中比较。

    public ResultUtil logout(HttpServletRequest request, HttpServletResponse response) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null) {
            new SecurityContextLogoutHandler().logout(request, response, auth);
            PermissionMap.map = null;
            PermissionMap.list = null;
        }
        return ResultUtil.success();
    }

接下来用postman请求测试一下:

登录

(1)当没有带token时user/hello也是可以访问的

当不带token访问user/list时:

当登录后携带token访问user/list时

当携带token且角色为REPORTADMIN

访问角色为ADMIN才能访问的role/list时

当携带token且角色为ADMIN 时

项目源码地址:https://gitee.com/lanran1/security-jwt

postman设置token: https://blog.csdn.net/qq_35494808/article/details/89153589

猜你喜欢

转载自blog.csdn.net/qq_35494808/article/details/81537415