Springboot + SpringSecurity权限项目框架搭建
目录
5、用户实体类修改实现 UserDetails(该接口是默认权限框架)
8、配置security登录授权过滤器 及 Jwt登录授权过滤器 及 添加自定义未登录 及未授权结果返回
10、登录验证码配置
一、项目介绍
该项目是管理系统 系统的搭建主要采用了主流的 Springboot + SpringSecurity + Oauths2.0 + JWT + MybatisPlus + Mysql + Redis + Rabbitmq + Maven + FastDfs + Nginx 等技术,这里我们主要展示的是权限框架的搭建
二、项目搭建(父子工程)
1、添加 pom 依赖
<!-- web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.48</version> <scope>runtime</scope> </dependency> <!-- mybatis-plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.0.5</version> </dependency> <!-- swagger2 --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.7.0</version> </dependency> <!-- Swagger第三方ui依赖 --> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>swagger-bootstrap-ui</artifactId> <version>1.9.6</version> </dependency> <!--security依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- jwt --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
2、修改 yml 配置
server: port: 8081 spring: #数据源配置 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/***?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: **** password: ****** hikari: #连接池名 pool-name: DateHikariCP #最小空闲连接数 minimum-idle: 5 #空闲连接存货最大时间 idle-timeout: 180000 #最大连接数 maximum-pool-size: 10 #从连接池返回的连接的自动提交 auto-commit: true #连接最大存活时间,0表示永久存活 max-lifetime: 1800000 #连接超时时间 connection-timeout: 30000 #测试连接是否可用的查询语句 connection-test-query: SELECT 1 # Mybatis-plus配置 mybatis-plus: #配置Mapper映射文件 mapper-locations: classpath*:/mapper/*Mapper.xml #配置Mybatis数据返回类型别名(默认别名是类名) type-aliases-package: com.hierway.pojo configuration: #自动驼峰命名 map-underscore-to-camel-case: false ## Mybatis SQL 打印(方法接口所在的包,不是Mapper.xml所在的包) logging: level: com.hierway.mapper: debug jwt: # JWT存储的请求头 tokenHeader: Authorization # JWT加密使用的密钥 secret: yeb-secret # JWT的超期限时间(60*60*24) expiration: 604800 # JWT 负载中拿到开头 tokenHead: Bearer
3、编写JwtTokenUtil工具类
/** * JwtToken配置类 */ @Component public class JwtTokenUtil { private static final String CLAIM_KEY_USERNAME = "sub"; private static final String CLAIM_KEY_CREATED = "created"; @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; /** * 根据用户信息生成Token * @param userDetails * @return */ public String generateToken(UserDetails userDetails){ Map<String,Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME,userDetails.getUsername()); claims.put(CLAIM_KEY_CREATED,new Date()); return generateToken(claims); } /** * 根据荷载生成JWT Token * @param claims * @return */ private String generateToken(Map<String,Object> claims){ return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS256,secret) .compact(); } /** * 验证Token是否有效 * @param token * @param userDetails * @return */ public boolean validateToken(String token,UserDetails userDetails){ String username = getUsernameFromToken(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } /** * 判断token是否可以被刷新 * @param token * @return */ public boolean canRefresh(String token){ return !isTokenExpired(token); } /** * 刷新token * @param token * @return */ public String refreshToken(String token){ Claims claims = getClaimsFromToken(token); claims.put(CLAIM_KEY_CREATED,new Date()); return generateToken(claims); } /** * 判断token是否失效 * @param token * @return */ private boolean isTokenExpired(String token) { Date expireDate = getExpiredFromToken(token); return expireDate.before(new Date()); } /** * 从token中获取过期时间 * @param token * @return */ private Date getExpiredFromToken(String token) { Claims claims = getClaimsFromToken(token); return claims.getExpiration(); } /** * 从token中获取登陆用户名 * @param token * @return */ public String getUsernameFromToken(String token){ String username; try{ Claims claims = getClaimsFromToken(token); username = (String) claims.get(CLAIM_KEY_USERNAME); //username = claims.getSubject(); } catch (Exception e){ username = null; } return username; } /** * 从token中获取荷载 * @param token * @return */ private Claims getClaimsFromToken(String token) { Claims claims = null; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } catch (Exception e) { e.printStackTrace(); } return claims; } private Date generateExpirationDate() { return new Date(System.currentTimeMillis() + expiration*1000); } }
4、公共返回对象
/** * @author guoce * @date 2022/11/30 11:24 * note : 公共返回对象 */ @Data @NoArgsConstructor @AllArgsConstructor public class RespBean { private Integer code; private String message; private Object obj; /** * 成功返回结果 * @param message message * @return RespBean */ public static RespBean success(String message){ return new RespBean(200,message,null); } /** * 成功返回结果 * @param message message * @param obj obj * @return RespBean */ public static RespBean success(String message,Object obj){ return new RespBean(200,message,obj); } /** * 失败返回结果 * @param message message * @return RespBean */ public static RespBean error(String message){ return new RespBean(418,message,null); } /** * 失败返回结果 * @param message message * @param obj obj * @return RespBean */ public static RespBean error(String message,Object obj){ return new RespBean(418,message,obj); } }
5、用户实体类修改实现 UserDetails(该接口是默认权限框架)
@EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("t_admin") @ApiModel(value="Admin对象", description="管理员表") public class Admin implements Serializable, UserDetails { private static final long serialVersionUID = 1L; @ApiModelProperty(value = "id") @TableId(value = "id", type = IdType.AUTO) private Integer id; @ApiModelProperty(value = "姓名") private String name; @ApiModelProperty(value = "手机号码") private String phone; @ApiModelProperty(value = "住宅电话") private String telephone; @ApiModelProperty(value = "联系地址") private String address; @ApiModelProperty(value = "用户名") private String username; @ApiModelProperty(value = "密码") private String password; @ApiModelProperty(value = "用户头像") private String userFace; @ApiModelProperty(value = "备注") private String remark; @ApiModelProperty(value = "是否启用1 0") private Boolean enabled; public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getTelephone() { return telephone; } public void setTelephone(String telephone) { this.telephone = telephone; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public String getUserFace() { return userFace; } public void setUserFace(String userFace) { this.userFace = userFace; } public String getRemark() { return remark; } public void setRemark(String remark) { this.remark = remark; } // public Boolean getEnabled() { // return enabled; // } public void setEnabled(Boolean enabled) { this.enabled = enabled; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return enabled; } }
6、登陆成功后获取token
a、创建登录实体类
@Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @ApiModel(value = "AdminLogin对象",description = "") public class AdminLoginParam { @ApiModelProperty(value = "用户名",required = true) private String username; @ApiModelProperty(value = "密码",required = true) private String password; @ApiModelProperty(value = "验证码",required = true) private String code; }
b、创建当前登录用户信息接口
1.controller层 @Api(tags = "LoginController") @RestController public class LoginController { @Autowired private IAdminService iAdminService; @ApiOperation(value = "登陆之后返回token") @PostMapping("/login") public RespBean login(@RequestBody AdminLoginParam adminLoginParam , HttpServletRequest request){ return iAdminService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),adminLoginParam.getCode(),request); } } 2.service层 public interface IAdminService extends IService<Admin> { /** * 登陆之后返回token * @param username * @param password * @param request * @return */ RespBean login(String username, String password, String code, HttpServletRequest request); } 3.serviceImpl层 @Service public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private PasswordEncoder passwordEncoder; @Value("${jwt.tokenHead}") private String tokenHead; /** * 登录之后返回token * @param username 用户名 * @param password 密码 * @param request 请求参数 * @return RespBean */ @Override public RespBean login(String username, String password, String code, HttpServletRequest request) { //获取到session中的验证码 String captcha = (String) request.getSession().getAttribute("captcha"); if(StringUtils.isEmpty(code) || !code.equalsIgnoreCase(captcha)){ return RespBean.error("验证码错误,请重新输入!"); } //登录 UserDetails userDetails = userDetailsService.loadUserByUsername(username); if(null == userDetails || !passwordEncoder.matches(password,userDetails.getPassword())){ return RespBean.error("用户名或密码不正确"); } if(!userDetails.isEnabled()){ return RespBean.error("账号被禁用,请联系管理员!"); } //更新security登陆用户对象(把登陆成功的用户放到security的全文中) UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null,userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); //生成token String token = jwtTokenUtil.generateToken(userDetails); Map<String,String> tokenMap = new HashMap<>(); tokenMap.put("token",token); tokenMap.put("tokenHead",tokenHead); return RespBean.success("登陆成功",tokenMap); } }
7、获取当前登录用户信息
1.controller层 @ApiOperation(value = "获取当前登录用户信息") @GetMapping("/admin/info") public Admin getAdminInfo(Principal principal){ if(principal == null){ return null; } String username = principal.getName(); Admin admin = iAdminService.getAdminByUsername(username); admin.setPassword(null); return admin; } /** * 与前端约定好 后端接口状态码200 来删除请求头里的token * @return */ @ApiOperation(value = "退出登录") @PostMapping("/logout") public RespBean logout(){ return RespBean.success("注销成功!"); } 2.service层 Admin getAdminByUsername(String username); 3.serviceImpl层 /** * 根据用户名获取用户 * @param username 用户名 * @return Admin */ @Override public Admin getAdminByUsername(String username) { return adminMapper.selectOne(new QueryWrapper<Admin>().eq("username",username).eq("enabled",1)); }
8、配置security登录授权过滤器 及 Jwt登录授权过滤器 及 添加自定义未登录 及未授权结果返回
1. security配置类 @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private IAdminService iAdminService; @Autowired private RestAuthorizationEntryPoint restAuthorizationEntryPoint; @Autowired private RestfulAccessDeniedHandler restfulAccessDeniedHandler; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { /** 使用JWT 不需要csrf */ http.csrf() .disable() /** 基于token 不需要session */ .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() /** 所有的请求都需要被认证 */ .authorizeRequests() .anyRequest() .authenticated() .and() /** 禁用缓存 */ .headers() .cacheControl(); /** 添加jwt 登录授权过滤器 */ http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class); /** 添加自定义未登录与未授权结果返回 */ http.exceptionHandling() .accessDeniedHandler(restfulAccessDeniedHandler) .authenticationEntryPoint(restAuthorizationEntryPoint); } /** * 重写了UserDetailsService中的方法,编写自己的登录逻辑 * @return */ @Bean @Override public UserDetailsService userDetailsService() { return username -> { Admin admin = iAdminService.getAdminByUsername(username); if(null != admin){ return admin; } throw new UsernameNotFoundException("用户名或密码不正确!"); }; } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Bean public JwtAuthencationTokenFilter jwtAuthencationTokenFilter(){ return new JwtAuthencationTokenFilter(); } } 2.jwt登录授权过滤器 /** * note:jwt登录授权过滤器 */ public class JwtAuthencationTokenFilter extends OncePerRequestFilter { @Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private UserDetailsService userDetailsService; /** * 所有的登录都需要走该过滤器 * note:判断该请求是否携带token,如果没有直接过滤掉,如果携带token,需要对该token进行校验,该token是否合法,如果合法则放行,否则过滤掉 * @param request * @param response * @param filterChain * @throws ServletException * @throws IOException */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String header = request.getHeader(tokenHeader); /** 判断请求头部是否携带token */ //存在token if(header != null && header.startsWith(tokenHead)){ String token = header.substring(tokenHead.length()); String username = jwtTokenUtil.getUsernameFromToken(token); //token存在用户名但未登录 if(username != null && null == SecurityContextHolder.getContext().getAuthentication()){ //登录 UserDetails userDetails = userDetailsService.loadUserByUsername(username); //验证token是否有效,重新设置用户对象 if(jwtTokenUtil.validateToken(token,userDetails)){ UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( userDetails,null,userDetails.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } } filterChain.doFilter(request,response); } } 3.自定义未登录过滤器 /** * note:当未登录或者token失效访问接口时,自定义的返回结果 */ @Component public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); PrintWriter writer = response.getWriter(); RespBean bean = RespBean.error("未登录,请登录!"); bean.setCode(401); writer.write(new ObjectMapper().writeValueAsString(bean)); writer.flush(); writer.close(); } } 4.自定义未授权过滤器 /** * note:未授权处理器配置类 */ @Component public class RestfulAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); PrintWriter writer = response.getWriter(); RespBean bean = RespBean.error("权限不足,请联系管理员!"); bean.setCode(403); writer.write(new ObjectMapper().writeValueAsString(bean)); writer.flush(); writer.close(); } }
9、配置接口文档swagger
1.security配置类 /** * 配置释放的访问路径(swagger接口文档释放口,及登录退出页面释放口) * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers( "/login", "/logout", "/css/**", "/js/**", "/index.html", "favicon.ico", "/doc.html", "/webjars/**", "/swagger-resources/**", "/v2/api-docs/**", "/captcha" ); } 2.swagger配置类 /** * note:swagger配置类 */ @Configuration @EnableSwagger2 public class Swagger2Config { @Bean public Docket createRestApi(){ return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.hierway.controller")) .paths(PathSelectors.any()) .build() .securityContexts(securityContexts()) .securitySchemes(securitySchemes()); } private ApiInfo apiInfo(){ return new ApiInfoBuilder() .title("海量接口文档") .description("海量接口文档") .contact(new Contact("GuoCe","http://localhost:8081/doc.html","hailiang@hailiang.com")) .version("1.0") .build(); } private List<ApiKey> securitySchemes(){ //设置请求头信息 List<ApiKey> result = new ArrayList<>(); ApiKey apiKey = new ApiKey("Authorization","Authorization","Header"); result.add(apiKey); return result; } private List<SecurityContext> securityContexts(){ //设置需要登录认证的路径 List<SecurityContext> result = new ArrayList<>(); result.add(getContextByPath("/hello/.*")); return result; } private SecurityContext getContextByPath(String pathRegex) { return SecurityContext.builder() .securityReferences(defaultAuth()) .forPaths(PathSelectors.regex(pathRegex)) .build(); } private List<SecurityReference> defaultAuth() { List<SecurityReference> result = new ArrayList<>(); AuthorizationScope authorizationScope = new AuthorizationScope("global","accessEverything"); AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; authorizationScopes[0] = authorizationScope; result.add(new SecurityReference("Authorization",authorizationScopes)); return result; } }
10、登录验证码配置
1.添加pom依赖 <!-- google 验证码依赖--> <dependency> <groupId>com.github.axet</groupId> <artifactId>kaptcha</artifactId> <version>0.0.9</version> </dependency> 2.图形验证码配置类 /** * note:图形验证码Controller */ @Configuration public class CaptchaConfig { @Bean public DefaultKaptcha defaultKaptcha(){ //验证码生成器 DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); //配置 Properties properties = new Properties(); //是否有边框 properties.setProperty("kaptcha.border","yes"); //设置边框颜色 properties.setProperty("kaptcha.border.color","105,179,90"); //验证码 properties.setProperty("kaptcha.session.key","code"); //验证码文本字符颜色,默认为黑色 properties.setProperty("kaptcha.textproducer.font.color","blue"); //设置字体样式 properties.setProperty("kaptcha.textproducer.font.names","宋体,楷体,微软雅黑"); //字体大小,默认为40 properties.setProperty("kaptcha.textproducer.font.size","30"); //字符长度,默认为5 properties.setProperty("kaptcha.textproducer.char.length","4"); //字符间距 properties.setProperty("kaptcha.textproducer.char.space","4"); //验证码图片宽度,默认为200 properties.setProperty("kaptcha.image.width","100"); //验证码图片高度 默认为40 properties.setProperty("kaptcha.image.height","40"); Config config = new Config(properties); defaultKaptcha.setConfig(config); return defaultKaptcha; } } 3.图形验证码接口 @RestController public class CaptchaController { @Autowired private DefaultKaptcha defaultKaptcha; @ApiOperation(value = "获取验证码") @GetMapping(value = "/captcha",produces = "image/jpeg") public void captcha(HttpServletRequest request, HttpServletResponse response){ //定义response输出类型为image/jpeg类型 response.setDateHeader("Expires",0); //Set standard HTTP/1.1 no-cache headers response.setHeader("Cache-Control","no-store,no-cache,must-revalidate"); //Set IE extended HTTP/1.1 no-cache headers (use addHeader) response.addHeader("Cache-Control","post-check=0,pre-check=0"); //Set IE extended HTTP/1.1 no-cache header response.setHeader("Pragma","no-cache"); //return a jpeg response.setContentType("image/jpeg"); /** ===============================生成验证码 begin============================ */ //获取验证码文本内容 String text = defaultKaptcha.createText(); System.out.println("验证码内容:" + text); //将验证码文本内容放入session /** captcah */ request.getSession().setAttribute("captcha",text); //根据文本验证码内容创建图形验证码 BufferedImage image = defaultKaptcha.createImage(text); ServletOutputStream outputStream = null; try { outputStream = response.getOutputStream(); //输出流输出图片 ImageIO.write(image,"jpg",outputStream); outputStream.flush(); } catch (IOException e) { e.printStackTrace(); }finally { if(null != outputStream){ try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } /** ===============================生成验证码 end============================== */ } } 4.swagger配置类释放该接口 /** * 配置释放的访问路径(swagger接口文档释放口,及登录退出页面释放口) * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers( "/login", "/logout", "/css/**", "/js/**", "/index.html", "favicon.ico", "/doc.html", "/webjars/**", "/swagger-resources/**", "/v2/api-docs/**", "/captcha" ); } 5.登录接口进行校验 //获取到session中的验证码 String captcha = (String) request.getSession().getAttribute("captcha"); if(StringUtils.isEmpty(code) || !code.equalsIgnoreCase(captcha)){ return RespBean.error("验证码错误,请重新输入!"); }