单车

单车第五天

2018-10-01  本文已影响20人  shenyoujian

转自http://coder520.com/
1、修改用户信息
先想一个问题,要修改用户信息得先从移动端传递用户id过来,之后才能进行获取用户然后进行修改,但是如果移动端出错了,本来01想修改用户名但是它出错传递了个03过来,然后我服务端修改了03的信息,这就不好了吧。这种修改信息的,传递的id最好从后台获取。不然不安全。
解决办法:从后台获取id,可以从token获取这个id,而token可以从移动端放入headers传递过来。可以先这样做,写一个baseController里面写一个getCurrent方法,这个方法的作用就是从token里获取用户id。

2、controller,注意我们与移动端约定好用json传输,所以不能使用String类型参数,必须使用requestbody把json封装成对象。

public ApiResult modifyUsername(@RequestBody User user){
        ApiResult resp = new ApiResult();
        try {
            userService.modifyUsername(user);
        } catch (MaMaBikeException e) {
            //校验失败
            log.error(e.getMessage());
            resp.setCode(Constants.RESP_STATUS_INTERNAL_ERROR);
            resp.setMessage(e.getMessage());
        } catch (Exception e) {
            // 登录失败,返回失败信息,就不用返回data
            // 记录日志
            log.error("Fail to modifyUsername", e);
            resp.setCode(Constants.RESP_STATUS_INTERNAL_ERROR);
            resp.setMessage("内部错误!");
        }

        return resp;
    }

service

/**
     * Author ljs
     * Description 修改用户信息业务
     * Date 2018/9/25 16:30
     **/
    @Override
    public void modifyUsername(User user) throws MaMaBikeException{
        userMapper.updateByPrimaryKeySelective(user);
    }

3、上面的controller是不完善的,因为user里只有newusername和headers里的token(约定移动端不能传递userid),用户id由我们后端自己获取。定义获取用户的方法,因为这个方法会被多处用到,所以放在basecontroller里。因为只允许继承的类使用,所以方法定义为protected好一点。

public class BaseController {

    @Autowired
    private CommonCacheUtil redis;

    /**
     * Author ljs
     * Description 根据token获取user
     * Date 2018/9/25 20:10
     **/
    protected UserElement getCurrenUser(){
        //1、使用springmvc提供的类去获取request
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        //2、从header里获取token
        String token = request.getHeader(Constants.REQUEST_TOKEN_KEY);
        if (!StringUtils.isBlank(token)) {
            //3、不为空,根据token去redis获取用户
            try{
                UserElement ue = redis.getUserByToken(token);
                return ue;
            }catch (Exception e){
                return null;
            }
        }
        return null;
    }
}

创建getUserByToken方法

/**
     * Author ljs
     * Description 根据token获取缓存的用户
     * Date 2018/9/25 21:23
     **/
    public UserElement getUserByToken(String token) throws MaMaBikeException {
        UserElement ue = null;
        JedisPool pool = jedisPoolWrapper.getJedisPool();
        if (pool != null) {
            //1.7支持try()括号里的内容在try之后自动关闭流或者资源,不用自动关闭
            try (Jedis jedis = pool.getResource()) {
                jedis.select(0);
                //根据key从redis获取Map
                try {
                    Map<String, String> map = jedis.hgetAll(TOKEN_PREFIX + token);
                    if (!CollectionUtils.isEmpty(map)) {
                        //把map转对象
                        ue = UserElement.fromMap(map);
                    }else{
                        log.warn("Fail to find cached element for token {}", token);
                    }

                } catch (Exception e) {
                    log.error("Fail to get token from redis", e);
                    throw new MaMaBikeException("Fail to get token content");
                }
            }
        }
        return ue;
    }

4、最后完善controller,就可以根据id去更新用户的信息了。添加headers和传递要修改的新名字

 //根据token获取用户id
            UserElement ue = getCurrenUser();
            user.setId(ue.getUserId());
            userService.modifyUsername(user);

启动服务器测试


image.png
image.png image.png

ok。

5、登录方法是不用拦截,而修改个人信息是需要拦截的,现在我们用不了之前通过url来拦截,我们这是对接app的。所以我们可以通过判断token是否正确来进行拦截。

6.1、整合springsecurity,加入pom

<!--整合springSecurity-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

springsecurity教程
spring4all http://www.spring4all.com/article/428
spring官方文档https://docs.spring.io/spring-security/site/docs/4.2.8.RELEASE/reference/htmlsingle/

6.2、创建配置类

/**
 * @Author ljs
 * @Description security配置类
 * @Date 2018/9/28 16:33
 **/

@Configuration              //springboot启动时加载该配置类
@EnableWebSecurity          //启动springsecurity的web安全支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //web安全配置的细节,如定义哪些url路径应该被保护,哪些不应该。
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭默认打开的crsf保护
       http.csrf().disable()
               //允许含login不需要身份验证
               .authorizeRequests()
               .antMatchers("/**/login").permitAll()
               //其他请求都需要身份验证
               .anyRequest().authenticated()
               .and()
               //创建成无状态的请求,即不创建session,因为我们是和移动端对接的。
               .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
               ;
    }
}

具体:
自定义filter实现抽象类PreAuthenticatedProcessingFilter的getPreAuthenticatedPrincipal方法,该方法是提取用户提交需要校验的信息,然后传递给自定义的provider实现AuthenticationProvider的supports方法,该方法是校验filter传递过来的对象,校验成功返回true,接着调用Authentication进行授权。其中filter提取不到,provider校验失败等都会调用自定义的entrypoint实现AuthenticationEntryPoint的commence方法告诉客户端校验失败。

最后把自定义的filter,provider,entrypoint配置到security的配置类里。
因为provider可以有多个,需要一个叫manager的List来管理

//往manager里添加provider
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(new RestAuthenticationProvider());
    }

那么现在filter就不是调用provider而是manager,需要给filter设置manager

//给filter里设置manager
    private RestPreAuthenticatedProcessingFilter getPreAuthenticatedProcessingFilter() throws Exception {
        RestPreAuthenticatedProcessingFilter filter = new RestPreAuthenticatedProcessingFilter();
        filter.setAuthenticationManager(this.authenticationManagerBean());
        return filter;
    }

configure配置filter和entrypoint

.and().httpBasic().authenticationEntryPoint(new RestAuthenticationEntryPoint())
.and().addFilter(getPreAuthenticatedProcessingFilter())

debug接下来验证流程:
请求modifyusername


image.png

接着进到抽象类里方法,该方法用于判断提取的信息是否为空


image.png
因为我们return的是null,所以毫无疑问来到entrypoint
image.png
接下来验证login

同样是111111之后就来到了controller而不会到entrypoint,因为我们在配置类里声明了login允许访问。


image.png
 .antMatchers("/**/login").permitAll()

ok,认证授权流程验证成功。

7、写活无需拦截的url
我们把无需拦截的url写在代码不友好,后续如果要添加其他无需拦截的url,还得修改代码,继续添加,我们可以写在一个配置文件里,不要写在yml里,因为yml识别不了**,写在properties

 .antMatchers("/**/login").permitAll()

7.1、parameter.properties文件,以后还有其他无需拦截就再后面添加就行了。

#security无需拦截的url
security.noneSecurityPath=/**/login

7.2、加载properties文件

@PropertySource(value = "classpath:parameter.properties")

7.3、读取properties文件,因为antMatchers需要传递一个String类型的列表

@Value("#'${security.noneSecurityPath}'.split(',')")
private List noneSecurityPath;

7.4、为了能使用split(',')和占位符,再加载的时候需要进行配置

/**
     * 用于properties文件占位符解析
     * @return
     */
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertyConfigInDev() {
        return new PropertySourcesPlaceholderConfigurer();
    }

7.5、最后在security配置类注入Parameters类,并且读取noneSecurityPath,把读取的List对象转换为String数组传进antMatchers

 //注入Parameter类
    @Autowired
    private Parameters parameters;


.antMatchers((String[])parameters.getNoneSecurityPath().toArray(new String[parameters.getNoneSecurityPath().size()])).permitAll()//符合条件的路径放过验证

7.6、最后debugger测试login可以通过,ok

8、处理无需验证的请求
上面已经验证了认证授权的流程,还写活了无需拦截的url,当我们login请求之后,filter同样会拦截下来,但是我们返回null,所以它还是会进去方法判断,然后打出日志意思就是从这个请求中获取不到任何验证的信息

16:24 DEBUG [c.l.m.s.RestPreAuthenticatedProcessingFilter] No pre-authenticated principal found in request

我们需要给这些无需验证直接放过的请求做特殊处理,
解决:判断url是否是无需验证的url,是就随便给一个权限然后让它们通过,不然不会调用接下来的provider,打印错误日志造成逻辑不通。

8.1、首先filter需要知道哪些url是无需验证的,我们得先注入parameter,但是filter这个是独立于spring容器,不能使用getset注入,但是可以使用构造器注入

 private List<String> noneSecurityList;

    //使用构造器注入,不能使用setget不然注不进去
    //因为这个filter在spring容器加载的时候就加载了。
    public RestPreAuthenticatedProcessingFilter(List<String> noneSecurityList) {
        this.noneSecurityList = noneSecurityList;
    }

security配置类里的filter同样需要传入参数

8.2、开始判断过来的请求在不在list里,我们可以使用spring的AntPathMatcher工具类帮我们匹配,它很好的封装了特殊字符的匹配

//spring路径匹配器
private AntPathMatcher matcher = new AntPathMatcher();

/**
     * Author ljs
     * Description 校验是否无需权限的uri
     * Date 2018/9/30 16:59
     **/
    private boolean isNoneSecurity(String uri) {
        boolean result = false;
        if(this.noneSecurityList!=null){
            for(String pattern:this.noneSecurityList){
                if(matcher.match(pattern,uri)){
                    result = true;
                    break;
                }
            }
        }
        return result;
    }

8.3、开始三种处理,filter就是判断url类型,并且提取信息封装到我们自定义的token里,然后发送到provider

 /**提取用户提交的信息,然后交给provider做校验,校验不通过进入entrypoint做异常处理**/
    @Override
    protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(1);


        /**第一种情况:无需拦截的请求**/
        if (isNoneSecurity(request.getRequestURL().toString()) || "OPTIONS".equals(request.getMethod())) {
            GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_SOMEONE");
            authorities.add(authority);
            //无需权限的url直接发放token走Provider授权
            return new RestAuthenticationToken(authorities);
        }


        /**第二种情况:需要拦截的请求**/
        String version = request.getHeader(Constants.REQUEST_VERSION_KEY);
        String token = request.getHeader(Constants.REQUEST_TOKEN_KEY);


        if (version == null) {
            request.setAttribute("header-error", 400);
        }

        /**校验token,如果header-error==null说明version是有值的**/
        if (request.getAttribute("header-error") == null) {
            try {
                if (token != null && !token.trim().isEmpty()) {
                    UserElement ue = redis.getUserByToken(token);

                    if (ue instanceof UserElement) {
                        //检查到token说明用户已经登录 授权给用户BIKE_CLIENT角色 允许访问
                        GrantedAuthority authority = new SimpleGrantedAuthority("BIKE_CLIENT");
                        authorities.add(authority);
                        RestAuthenticationToken authToken = new RestAuthenticationToken(authorities);
                        authToken.setUser(ue);
                        return authToken;
                    }
                } else {
                    log.warn("Got no token from request header");
                    //token不存在 告诉移动端 登录
                    request.setAttribute("header-error", 401);
                }
            } catch (Exception e) {
                log.error("Fail to authenticate user", e);
            }

        }

        /**第三种情况:给400和401一个角色,反正不能返回null,一定得返回一个token**/
        if (request.getAttribute("header-error") != null) {
            //请求头有错误  随便给个角色 让逻辑继续
            GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_NONE");
            authorities.add(authority);
        }
        RestAuthenticationToken authToken = new RestAuthenticationToken(authorities);
        return authToken;
    }

8.4、provider,因为我filter传递的是自定义的token对象,所以都需要判断一下

/**
     * Author ljs
     * Description 校验filter传递过来的对象,校验成功返回true
     * Date 2018/10/1 13:06
     **/
    @Override
    public boolean supports(Class<?> authentication) {
        return PreAuthenticatedAuthenticationToken.class.isAssignableFrom(authentication) || RestAuthenticationToken.class.isAssignableFrom(authentication);
    }

8.5、开始授权,role_none抛出一个自定义的异常就行

/**
     * Author ljs
     * Description 对符合要求的合法token授权
     * Date 2018/10/1 13:06
     **/
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        if (authentication instanceof PreAuthenticatedAuthenticationToken) {
            PreAuthenticatedAuthenticationToken preAuth = (PreAuthenticatedAuthenticationToken) authentication;
            RestAuthenticationToken sysAuth = (RestAuthenticationToken) preAuth.getPrincipal();
            //开始判断用户角色
            if (sysAuth.getAuthorities() != null && sysAuth.getAuthorities().size() > 0) {
                GrantedAuthority authority = sysAuth.getAuthorities().iterator().next();
                if ("BIKE_CLIENT".equals(authority.getAuthority())) {
                    return sysAuth;
                } else if ("ROLE_SOMEONE".equals(authority.getAuthority())) {
                    return sysAuth;
                }
            }
        } else if (authentication instanceof RestAuthenticationToken) {
            RestAuthenticationToken sysAuth = (RestAuthenticationToken) authentication;
            if (sysAuth.getAuthorities() != null && sysAuth.getAuthorities().size() > 0) {
                GrantedAuthority gauth = sysAuth.getAuthorities().iterator().next();
                if ("BIKE_CLIENT".equals(gauth.getAuthority())) {
                    return sysAuth;
                } else if ("ROLE_SOMEONE".equals(gauth.getAuthority())) {
                    return sysAuth;
                }
            }
        }

        throw new BadCredentialException("unknown.error");
    }

8.6、entrypoint

@Slf4j
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        ApiResult result = new ApiResult();
        //检查头部错误
        if (request.getAttribute("header-error") != null) {
            result.setCode(408);
            result.setMessage("请升级至app最新版本");
        } else {
            result.setCode(401);
            result.setMessage("请您登录");
        }

        try {
            //设置跨域请求 请求结果json刷到响应里
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, HEADER");
            response.setHeader("Access-Control-Max-Age", "3600");
            response.setHeader("Access-Control-Allow-Headers", "X-Requested-With, user-token, Content-Type, Accept, version, type, platform");
            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            response.getWriter().write(JSON.toJSONString(result));
            response.flushBuffer();
        } catch (Exception er) {
            log.error("Fail to send 401 response {}", er.getMessage());
        }

    }
}

security配置类

 //当我们设置了跨域之后,移动端会先发一个options请求来探测一下
    //确认你允不允许我跨域,都支持跨域哪些方法,所以我们需要不能拦截options方法的请求
    @Override
    public void configure(WebSecurity web) throws Exception {
        //忽略 OPTIONS 方法的请求
        web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
        //放过swagger
    }

debug测试:
先是不带version的modifyNickName


image.png

带version


image.png

但是测试login的时候,它并没有走第一种情况:无需拦截的请求,原来是
uri写错成url,urlhttp://localhost:8888/user/login,uri就只有user/login,改过来就行了。

isNoneSecurity(request.getRequestURL().toString())

总结:filter->manager->provider 出错就到entrypoint
把它背了。。。以后所有移动端套用就行了。

上一篇下一篇

猜你喜欢

热点阅读