转自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)
;
}
}
- 注意:crsf跨站脚本攻击,就是那些钓鱼网站,如果我的网站有一个表单需要用户提交,他们就仿造我这个表单去欺骗用户填写信息然后请求我的表单。解决办法就是给表单添加隐藏域字段,该字段由服务端生成一个唯一的标识,然后每一个请求都判断是否有该字段,并且该标识是否正确。
-
然后debug,请求modeifyusername,可以看到
image.png
而login是可以访问的到,ok测试成功。
6.3、上面我们只是完成了拦截,接下来需要校验token,当token正确的时候,可以让他除login之外的请求通过。 -
认证+授权的流程
大致:
filter->manager->provider(一个或N个提供权限信息) entrypoint(统一异常处理,前面三个只要有每一个出错都会来到这里)
image.png
具体:
自定义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
把它背了。。。以后所有移动端套用就行了。
网友评论