我们知道,在restful服务中,服务端不再直接生成页面了,而是只返回数据(json),客户端渲染,
所以我们需要定义数据格式
修改之前项目,之前已经引入过JWT了所以无需再引
通过之前文章,我们知道配置的重点是自定义
UserDetailsService
通过百度我们知道是UsernamePasswordAuthenticationFilter帮我们认证了用户,并且生成了页面,转发等等。
但是!在本例中,因为我们是整合restful服务,返回的都是json数据,所以我们不再需要这些。而既然要返回json数据,那我们先定义好一个通用的数据类,ResultVO:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResultVO<T> {
//10成功,20需要登陆 30无权限等等
private Integer code;
private String msg;
private T data;
}
首先写一个JwtUtils工具
package com.allmodel.models.authority.config.security.utils;
import com.allmodel.models.authority.config.security.entity.AuthUser;
import com.allmodel.models.authority.entity.RoleEntity;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.*;
import java.util.stream.Collectors;
/**
* @Author WQY
* @Date 2019/11/26 15:39
* @Version 1.0
*/
public class JwtUtil {
private static final String secret = "wlzl.com";
/**
* 生成token
* @param username
* @param roles
* @return
*/
public static String generateToken(String username,List<RoleEntity> roles) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", roles.parallelStream().map(RoleEntity::getName).collect(Collectors.joining(",")));
return Jwts.builder()
.setClaims(claims)
.setSubject(username)
//.setAudience(username+".")
//创建时间
.setIssuedAt(new Date())
//过期时间,我们设置为 五分钟
.setExpiration(new Date(System.currentTimeMillis() + 5 * 60 * 1000))
//签名,通过密钥保证安全性
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 解析token
* @param token
* @return
*/
public static AuthUser parseToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
String username = claims.getSubject();
//String audience = claims.getAudience();
//System.out.println(audience);
String roles = (String) claims.get("roles");
//因为生成的时候没有放入密码,所以不需要密码
return new AuthUser(username, null, Arrays.stream(roles.split(",")).map(name -> {
RoleEntity role = new RoleEntity();
role.setName(name);
return role;
}).collect(Collectors.toList()));
}
}
然后是获取生成token的controller
package com.allmodel.models.authority.controller;
import com.allmodel.models.authority.config.security.entity.ResultVO;
import com.allmodel.models.authority.config.security.utils.JwtUtil;
import com.allmodel.models.authority.dao.Role_Jpa;
import com.allmodel.models.authority.dao.User_Jpa;
import com.allmodel.models.authority.entity.RoleEntity;
import com.allmodel.models.authority.entity.UserEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
/**
* @Author WQY
* @Date 2019/11/26 15:42
* @Version 1.0
*/
@RestController
public class UserController {
@Resource
private User_Jpa userRepository;
@Resource
private Role_Jpa roleRepository;
@GetMapping("/token")
public ResultVO login(String username, String password) {
UserEntity user = userRepository.findByUsername(username);
if (user == null || !user.getPassword().equals(password)) {
ResultVO<Object> result = new ResultVO<>();
result.setCode(10);
result.setMsg("用户名或密码错误");
return result;
}
//Jwts.
ResultVO<Object> success = new ResultVO<>();
//用户名密码正确,生成token给客户端
success.setCode(0);
List<RoleEntity> roles = Collections.singletonList(roleRepository.findById(user.getAuthority()).get());
success.setData(JwtUtil.generateToken(username, roles));
return success;
}
}
我们定义两个异常处理,一个是token异常,一个是权限异常(就是有没有权限)
package com.allmodel.models.authority.config.security.handler;
import com.allmodel.models.authority.config.security.entity.ResultVO;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author WQY
* @Date 2019/11/26 15:43
* @Version 1.0
*/
@Component
public class AccessDeniedHandler implements org.springframework.security.web.access.AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("text/html;charset=UTF-8");
// 返回我们的自定义json
ObjectMapper objectMapper = new ObjectMapper();
ResultVO<Object> result = new ResultVO<>();
//50,标识有token,但是该用户没有权限
result.setCode(50);
result.setMsg("请求无效,用户没有权限");
response.getWriter().write(objectMapper.writeValueAsString(result));
}
}
package com.allmodel.models.authority.config.security.handler;
import com.allmodel.models.authority.config.security.entity.ResultVO;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author WQY
* @Date 2019/11/26 15:43
* @Version 1.0
*/
@Component
public class TokenExceptionHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("text/html;charset=UTF-8");
// 直接返回 json错误
ResultVO<Object> result = new ResultVO<>();
//20,标识没有token
result.setCode(20);
result.setMsg("请求无效,没有有效token");
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(result));
}
}
然后通过百度得知,在UsernamePasswordAuthenticationFilter中,springsecurity加入了SecurityContext,既然我们现在不用了,那我们要自己定义拦截器并且加入
securityContext以便springsecurity作权限处理,所以我们自定义拦截器:
package com.allmodel.models.authority.config.security.filter;
import com.allmodel.models.authority.config.security.entity.AuthUser;
import com.allmodel.models.authority.config.security.utils.JwtUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author WQY
* @Date 2019/11/26 15:44
* @Version 1.0
*/
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token");
//获取token,并且解析token,如果解析成功,则放入 SecurityContext
if (token != null) {
try {
AuthUser authUser = JwtUtil.parseToken(token);
//如果此处不放心解析出来的 authuser,可以再从数据库查一次,验证用户身份:
//解析成功
if (SecurityContextHolder.getContext().getAuthentication() == null) {
//我们依然使用原来filter中的token对象
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(authUser, null, authUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
} catch (Exception e) {
logger.info("解析失败,可能是伪造的或者该token已经失效了(我们设置失效5分钟)。");
}
}
filterChain.doFilter(request, response);
}
}
然后修改
SecurityConfig
由于我集成了Swagger2所以我把Swagger2的相关东西开放了
package com.allmodel.models.authority.config.security;
import com.allmodel.models.authority.config.security.filter.JwtTokenFilter;
import com.allmodel.models.authority.config.security.handler.AccessDeniedHandler;
import com.allmodel.models.authority.config.security.handler.TokenExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
/**
* @Author WQY
* @Date 2019/11/26 11:12
* @Version 1.0
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private TokenExceptionHandler tokenExceptionHandler;
@Resource
private AccessDeniedHandler accessDeniedHandler;
@Resource
private JwtTokenFilter jwtTokenFilter;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 因为我们的token是无状态的,不需要跨站保护
.csrf().disable()
// 添加异常处理,以及访问禁止(无权限)处理
.exceptionHandling().authenticationEntryPoint(tokenExceptionHandler).accessDeniedHandler(accessDeniedHandler).and()
// 我们不再需要session了
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
//定义拦截页面,所有api全部需要认证
.authorizeRequests()
.anyRequest().authenticated();
//最后,我们定义 filter,用来替换原来的UsernamePasswordAuthenticationFilter
httpSecurity.addFilterAt(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
public void configure(WebSecurity web) {
web.ignoring()
// 让我们获取 token的api不走springsecurity的过滤器,大道开放
.antMatchers(HttpMethod.GET,
"/token",
"/webjars/**",
"/resources/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/v2/api-docs"
);
}
}
//@Slf4j
//@EnableWebSecurity
//@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
//public class SecurityConfig extends WebSecurityConfigurerAdapter {
//
// private Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
//
// @Override
// protected void configure(HttpSecurity http) throws Exception {
// logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
// http
// .formLogin()
// .loginPage("/login")
// .loginProcessingUrl("/login")//处理登录请求接口
// .successForwardUrl("/hello")
// .defaultSuccessUrl("/hello",true)
// .and()
// .authorizeRequests()
// .antMatchers("/index","/login","/error").permitAll()
// .anyRequest()
// .authenticated()
// .and()
// .csrf()
// .disable();;
//
http.authorizeRequests() // 定义哪些URL需要被保护、哪些不需要被保护
.antMatchers("/login","/error","/index").permitAll()// 设置所有人都可以访问登录页面
.anyRequest().authenticated() // 任何请求,登录后可以访问
.and()
.formLogin().loginPage("/login");
;
// }
//}
然后写一个controller进行测试
@PreAuthorize("hasRole('ROLE_ADMIN')")此处就是什么什么权限的用户进行访问
最后我会把sql放上
ROLE_ADMIN中,ROLE_是Security默认添加的,数据库表中的是admin
package com.allmodel.models.authority.controller;
import com.allmodel.models.authority.config.security.entity.AuthUser;
import com.allmodel.models.authority.config.security.entity.ResultVO;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author WQY
* @Date 2019/11/26 15:45
* @Version 1.0
*/
@RestController
@RequestMapping
public class PermissionController {
@GetMapping("/permission")
public ResultVO loginTest(@AuthenticationPrincipal AuthUser authUser) {
ResultVO<String> resultVO = new ResultVO<>();
resultVO.setCode(0);
resultVO.setData("你成功访问了该api,这代表你已经登录,你是: " + authUser);
return resultVO;
}
@GetMapping("/permission2")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public ResultVO loginTest() {
ResultVO<String> resultVO = new ResultVO<>();
resultVO.setCode(0);
resultVO.setData("你成功访问了需要有 admin 角色的api。");
return resultVO;
}
}
然后我们访问hello,因为我们没有获取token切传入token所以会出现20
我们先获取token
用户名密码错误
密码不对,因为数据库现在放的是之前版本的密码,是加密过的
所以现在要把数据库中的加密密码改成123456
然后在登陆
就获取到token了
然后访问/permission
因为下面访问带消息头,所以我用的postman测试
由于我随便在修改了token所以现在token无效的
然后把刚刚获取的token放进去
然后访问/permission2
由于该账号具有admin权限所以可以访问
然后我们换一个没有权限的用户
先获取这个用户的token
然后访问
到此就整合完成了。
sql文件:
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 50717
Source Host : localhost:3306
Source Schema : authority
Target Server Type : MySQL
Target Server Version : 50717
File Encoding : 65001
Date: 27/11/2019 12:18:28
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'ROLE_ADMIN');
INSERT INTO `role` VALUES (2, 'ROLE_USER');
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
`savetime` datetime(0) NULL DEFAULT NULL COMMENT '保存时间',
`iphone` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号',
`authority` int(2) NULL DEFAULT NULL COMMENT '权限,1是最大权限,2是最小权限',
`sex` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '性别,1男,2女',
`organization_num` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '组织机构编号',
`organization_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '组织机构名称',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'admin', '123456', '2019-09-09 14:40:50', '15373065417', 1, '1', '1000', '物联智略');
INSERT INTO `user` VALUES (10, 'wqy', '123456', NULL, '', 2, '男', '10000007', '武邑环保局');
INSERT INTO `user` VALUES (11, '123', '321', '2019-11-25 12:37:35', NULL, 2, '312', '231', '312');
SET FOREIGN_KEY_CHECKS = 1;