基于Spring Security前后端分离的权限控制系统问题

目录
  • 1. 引入maven依赖
  • 2. 建表并生成相应的实体类
  • 3. 自定义UserDetails
  • 4. 自定义各种Handler
  • 5. Token处理
  • 6. 访问控制
  • 7. 配置WebSecurity
  • 8. 看效果
  • 9. 补充:手机号+短信验证码登录

前后端分离的项目,前端有菜单(menu),后端有API(backendApi),一个menu对应的页面有N个API接口来支持,本文介绍如何基于Spring Security前后端分离的权限控制系统问题。

话不多说,入正题。一个简单的权限控制系统需要考虑的问题如下:

  1. 权限如何加载
  2. 权限匹配规则
  3. 登录

1. 引入maven依赖

<?xml version="1.0" encoding="UTF-8"?>
  <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.1</version>
        <relativePath/> <!-- lookup parent from repository -->
     </parent>
     <groupId>com.example</groupId>
     <artifactId>demo5</artifactId>
     <version>0.0.1-SNAPSHOT</version>
     <name>demo5</name>

     <properties>
         <java.version>1.8</java.version>
     </properties>

     <dependencies>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-jpa</artifactId>
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-redis</artifactId>
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-security</artifactId>
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
         </dependency>

         <dependency>
             <groupId>io.jsonwebtoken</groupId>
             <artifactId>jjwt</artifactId>
             <version>0.9.1</version>
         </dependency>

         <dependency>
             <groupId>com.alibaba</groupId>
             <artifactId>fastjson</artifactId>
             <version>1.2.76</version>
         </dependency>
         <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
             <version>3.12.0</version>
         </dependency>
         <dependency>
             <groupId>commons-codec</groupId>
             <artifactId>commons-codec</artifactId>
             <version>1.15</version>
         </dependency>

         <dependency>
             <groupId>mysql</groupId>
             <artifactId>mysql-connector-java</artifactId>
             <scope>runtime</scope>
         </dependency>
         <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
             <optional>true</optional>
         </dependency>
     </dependencies>

     <build>
         <plugins>
             <plugin>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
                 <configuration>
                     <excludes>
                         <exclude>
                             <groupId>org.projectlombok</groupId>
                             <artifactId>lombok</artifactId>
                         </exclude>
                     </excludes>
                 </configuration>
             </plugin>
         </plugins>
     </build>

 </project>

application.properties配置

server.port=8080
 server.servlet.context-path=/demo

 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
 spring.datasource.url=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8
 spring.datasource.username=root
 spring.datasource.password=123456

spring.jpa.database=mysql
 spring.jpa.open-in-view=true
 spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
 spring.jpa.show-sql=true

 spring.redis.host=192.168.28.31
 spring.redis.port=6379
 spring.redis.password=123456

2. 建表并生成相应的实体类

SysUser.java

package com.example.demo5.entity;

  import lombok.Getter;
  import lombok.Setter;

 import javax.persistence.*;
  import java.io.Serializable;
  import java.time.LocalDate;
  import java.util.Set;

 /**
  * 用户表
  * @Author ChengJianSheng
  * @Date 2021/6/12
  */
 @Setter
 @Getter
 @Entity
 @Table(name = "sys_user")
 public class SysUserEntity implements Serializable {

     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     @Column(name = "id")
     private Integer id;

     @Column(name = "username")
     private String username;

     @Column(name = "password")
     private String password;

     @Column(name = "mobile")
     private String mobile;

     @Column(name = "enabled")
     private Integer enabled;

     @Column(name = "create_time")
     private LocalDate createTime;

     @Column(name = "update_time")
     private LocalDate updateTime;

     @OneToOne
     @JoinColumn(name = "dept_id")
     private SysDeptEntity dept;

     @ManyToMany
     @JoinTable(name = "sys_user_role",
             joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
             inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")})
     private Set<SysRoleEntity> roles;

 }

SysDept.java

部门相当于用户组,这里简化了一下,用户组没有跟角色管理

package com.example.demo5.entity;

  import lombok.Data;

  import javax.persistence.*;
  import java.io.Serializable;
  import java.util.Set;

  /**
  * 部门表
  * @Author ChengJianSheng
  * @Date 2021/6/12
  */
 @Data
 @Entity
 @Table(name = "sys_dept")
 public class SysDeptEntity implements Serializable {

     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     @Column(name = "id")
     private Integer id;

     /**
      * 部门名称
      */
     @Column(name = "name")
     private String name;

     /**
      * 父级部门ID
      */
     @Column(name = "pid")
     private Integer pid;

 //    @ManyToMany(mappedBy = "depts")
 //    private Set<SysRoleEntity> roles;
 }

SysMenu.java

菜单相当于权限

package com.example.demo5.entity;

  import lombok.Data;
  import lombok.Getter;
  import lombok.Setter;

  import javax.persistence.*;
  import java.io.Serializable;
 import java.util.Set;

 /**
  * 菜单表
  * @Author ChengJianSheng
  * @Date 2021/6/12
  */
 @Setter
 @Getter
 @Entity
 @Table(name = "sys_menu")
 public class SysMenuEntity implements Serializable {

     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     @Column(name = "id")
     private Integer id;

     /**
      * 资源编码
      */
     @Column(name = "code")
     private String code;

     /**
      * 资源名称
      */
     @Column(name = "name")
     private String name;

     /**
      * 菜单/按钮URL
      */
     @Column(name = "url")
     private String url;

     /**
      * 资源类型(1:菜单,2:按钮)
      */
     @Column(name = "type")
     private Integer type;

     /**
      * 父级菜单ID
      */
     @Column(name = "pid")
     private Integer pid;

     /**
      * 排序号
      */
     @Column(name = "sort")
     private Integer sort;

     @ManyToMany(mappedBy = "menus")
     private Set<SysRoleEntity> roles;

 }

SysRole.java

package com.example.demo5.entity;

  import lombok.Data;
  import lombok.Getter;
  import lombok.Setter;

  import javax.persistence.*;
  import java.io.Serializable;
  import java.util.Set;

 /**
  * 角色表
  * @Author ChengJianSheng
  * @Date 2021/6/12
  */
 @Setter
 @Getter
 @Entity
 @Table(name = "sys_role")
 public class SysRoleEntity implements Serializable {

     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     @Column(name = "id")
     private Integer id;

     /**
      * 角色名称
      */
     @Column(name = "name")
     private String name;

     @ManyToMany(mappedBy = "roles")
     private Set<SysUserEntity> users;

     @ManyToMany
     @JoinTable(name = "sys_role_menu",
             joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},
             inverseJoinColumns = {@JoinColumn(name = "menu_id", referencedColumnName = "id")})
     private Set<SysMenuEntity> menus;

 //    @ManyToMany
 //    @JoinTable(name = "sys_dept_role",
 //            joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},
 //            inverseJoinColumns = {@JoinColumn(name = "dept_id", referencedColumnName = "id")})
 //    private Set<SysDeptEntity> depts;

 }

注意,不要使用@Data注解,因为@Data包含@ToString注解

不要随便打印SysUser,例如:System.out.println(sysUser); 任何形式的toString()调用都不要有,否则很有可能造成循环调用,死递归。想想看,SysUser里面要查SysRole,SysRole要查SysMenu,SysMenu又要查SysRole。除非不用懒加载。

3. 自定义UserDetails

虽然可以使用Spring Security自带的User,但是笔者还是强烈建议自定义一个UserDetails,后面可以直接将其序列化成json缓存到redis中

package com.example.demo5.domain;

  import lombok.Setter;
  import org.springframework.security.core.GrantedAuthority;
  import org.springframework.security.core.authority.SimpleGrantedAuthority;
  import org.springframework.security.core.userdetails.User;
  import org.springframework.security.core.userdetails.UserDetails;

  import java.util.Collection;
 import java.util.Set;

 /**
  * @Author ChengJianSheng
  * @Date 2021/6/12
  * @see User
  * @see org.springframework.security.core.userdetails.User
  */
 @Setter
 public class MyUserDetails implements UserDetails {

     private String username;
     private String password;
     private boolean enabled;
 //    private Collection<? extends GrantedAuthority> authorities;
     private Set<SimpleGrantedAuthority> authorities;

     public MyUserDetails(String username, String password, boolean enabled, Set<SimpleGrantedAuthority> authorities) {
         this.username = username;
         this.password = password;
         this.enabled = enabled;
         this.authorities = authorities;
     }

     @Override
     public Collection<? extends GrantedAuthority> getAuthorities() {
         return authorities;
     }

     @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;
     }
 }

都自定义UserDetails了,当然要自己实现UserDetailsService了。这里当时偷懒直接用自带的User,后面放缓存的时候才知道不方便。

package com.example.demo5.service;

  import com.example.demo5.entity.SysMenuEntity;
  import com.example.demo5.entity.SysRoleEntity;
  import com.example.demo5.entity.SysUserEntity;
  import com.example.demo5.repository.SysUserRepository;
  import org.apache.commons.lang3.StringUtils;
  import org.springframework.security.core.authority.SimpleGrantedAuthority;
  import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.stereotype.Service;

 import javax.annotation.Resource;
 import java.util.Set;
 import java.util.stream.Collectors;

 /**
  * @Author ChengJianSheng
  * @Date 2021/6/12
  */
 @Service
 public class MyUserDetailsService implements UserDetailsService {
     @Resource
     private SysUserRepository sysUserRepository;

     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         SysUserEntity sysUserEntity = sysUserRepository.findByUsername(username);
         Set<SysRoleEntity> roleSet = sysUserEntity.getRoles();
         Set<SimpleGrantedAuthority> authorities = roleSet.stream().flatMap(role->role.getMenus().stream())
                 .filter(menu-> StringUtils.isNotBlank(menu.getCode()))
                 .map(SysMenuEntity::getCode)
                 .map(SimpleGrantedAuthority::new)
                 .collect(Collectors.toSet());
         User user = new User(sysUserEntity.getUsername(), sysUserEntity.getPassword(), authorities);
         return user;
     }
 }

算了,还是改过来吧

package com.example.demo5.service;

  import com.example.demo5.domain.MyUserDetails;
  import com.example.demo5.entity.SysMenuEntity;
  import com.example.demo5.entity.SysRoleEntity;
  import com.example.demo5.entity.SysUserEntity;
  import com.example.demo5.repository.SysUserRepository;
  import org.apache.commons.lang3.StringUtils;
  import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.stereotype.Service;

 import javax.annotation.Resource;
 import java.util.Set;
 import java.util.stream.Collectors;

 /**
  * @Author ChengJianSheng
  * @Date 2021/6/12
  */
 @Service
 public class MyUserDetailsService implements UserDetailsService {
     @Resource
     private SysUserRepository sysUserRepository;

     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         SysUserEntity sysUserEntity = sysUserRepository.findByUsername(username);
         Set<SysRoleEntity> roleSet = sysUserEntity.getRoles();
         Set<SimpleGrantedAuthority> authorities = roleSet.stream().flatMap(role->role.getMenus().stream())
                 .filter(menu-> StringUtils.isNotBlank(menu.getCode()))
                 .map(SysMenuEntity::getCode)
                 .map(SimpleGrantedAuthority::new)
                 .collect(Collectors.toSet());
 //        return new User(sysUserEntity.getUsername(), sysUserEntity.getPassword(), authorities);
         return new MyUserDetails(sysUserEntity.getUsername(), sysUserEntity.getPassword(), 1==sysUserEntity.getEnabled(), authorities);
     }
 }

4. 自定义各种Handler

登录成功

package com.example.demo5.handler;

  import com.alibaba.fastjson.JSON;
  import com.example.demo5.domain.MyUserDetails;
  import com.example.demo5.domain.RespResult;
  import com.example.demo5.util.JwtUtils;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
 import org.springframework.stereotype.Component;

 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.concurrent.TimeUnit;

 /**
  * 登录成功
  */
 @Component
 public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

     private static ObjectMapper objectMapper = new ObjectMapper();

     @Autowired
     private StringRedisTemplate stringRedisTemplate;

     @Override
     public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
         MyUserDetails user = (MyUserDetails) authentication.getPrincipal();
         String username = user.getUsername();
         String token = JwtUtils.createToken(username);
         stringRedisTemplate.opsForValue().set("TOKEN:" + token, JSON.toJSONString(user), 60, TimeUnit.MINUTES);

         response.setContentType("application/json;charset=utf-8");
         PrintWriter writer = response.getWriter();
         writer.write(objectMapper.writeValueAsString(new RespResult<>(1, "success", token)));
         writer.flush();
         writer.close();
     }
 }

登录失败

package com.example.demo5.handler;

  import com.example.demo5.domain.RespResult;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import org.springframework.security.core.AuthenticationException;
  import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
  import org.springframework.stereotype.Component;

  import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.PrintWriter;

 /**
  * 登录失败
  */
 @Component
 public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

     private static ObjectMapper objectMapper = new ObjectMapper();

     @Override
     public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
         response.setContentType("application/json;charset=utf-8");
         PrintWriter writer = response.getWriter();
         writer.write(objectMapper.writeValueAsString(new RespResult<>(0, exception.getMessage(), null)));
         writer.flush();
         writer.close();
     }
 }

未登录

package com.example.demo5.handler;

  import com.example.demo5.domain.RespResult;
  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;
 import java.io.PrintWriter;

 /**
  * 未认证(未登录)统一处理
  * @Author ChengJianSheng
  * @Date 2021/5/7
  */
 @Component
 public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {

     private static ObjectMapper objectMapper = new ObjectMapper();

     @Override
     public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
         response.setContentType("application/json;charset=utf-8");
         PrintWriter writer = response.getWriter();
         writer.write(objectMapper.writeValueAsString(new RespResult<>(0, "未登录,请先登录", null)));
         writer.flush();
         writer.close();
     }
 }

未授权

package com.example.demo5.handler;

  import com.example.demo5.domain.RespResult;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import org.springframework.security.access.AccessDeniedException;
  import org.springframework.security.web.access.AccessDeniedHandler;
  import org.springframework.stereotype.Component;

  import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.PrintWriter;

 @Component
 public class MyAccessDeniedHandler implements AccessDeniedHandler {

     private static ObjectMapper objectMapper = new ObjectMapper();

     @Override
     public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
         response.setContentType("application/json;charset=utf-8");
         PrintWriter writer = response.getWriter();
         writer.write(objectMapper.writeValueAsString(new RespResult<>(0, "抱歉,您没有权限访问", null)));
         writer.flush();
         writer.close();
     }
 }

Session过期

package com.example.demo5.handler;

  import com.example.demo5.domain.RespResult;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import org.springframework.security.web.session.SessionInformationExpiredEvent;
  import org.springframework.security.web.session.SessionInformationExpiredStrategy;

  import javax.servlet.ServletException;
  import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.PrintWriter;

 public class MyExpiredSessionStrategy implements SessionInformationExpiredStrategy {

     private static ObjectMapper objectMapper = new ObjectMapper();

     @Override
     public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
         String msg = "登录超时或已在另一台机器登录,您被迫下线!";
         RespResult respResult = new RespResult(0, msg, null);
         HttpServletResponse response = event.getResponse();
         response.setContentType("application/json;charset=utf-8");
         PrintWriter writer = response.getWriter();
         writer.write(objectMapper.writeValueAsString(respResult));
         writer.flush();
         writer.close();
     }
 }

退出成功

package com.example.demo5.handler;

  import com.fasterxml.jackson.databind.ObjectMapper;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.data.redis.core.StringRedisTemplate;
  import org.springframework.security.core.Authentication;
  import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
  import org.springframework.stereotype.Component;

 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.PrintWriter;

 @Component
 public class MyLogoutSuccessHandler implements LogoutSuccessHandler {

     private static ObjectMapper objectMapper = new ObjectMapper();

     @Autowired
     private StringRedisTemplate stringRedisTemplate;

     @Override
     public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
         String token = request.getHeader("token");
         stringRedisTemplate.delete("TOKEN:" + token);

         response.setContentType("application/json;charset=utf-8");
         PrintWriter printWriter = response.getWriter();
         printWriter.write(objectMapper.writeValueAsString("logout success"));
         printWriter.flush();
         printWriter.close();
     }
 }

5. Token处理

现在由于前后端分离,服务端不再维持Session,于是需要token来作为访问凭证

token工具类

package com.example.demo5.util;

  import io.jsonwebtoken.*;

  import java.util.Date;
  import java.util.HashMap;
  import java.util.Map;
  import java.util.function.Function;

 /**
  * @Author ChengJianSheng
  * @Date 2021/5/7
  */
 public class JwtUtils {

     private static long TOKEN_EXPIRATION = 24 * 60 * 60 * 1000;
     private static String TOKEN_SECRET_KEY = "123456";

     /**
      * 生成Token
      * @param subject   用户名
      * @return
      */
     public static String createToken(String subject) {
         long currentTimeMillis = System.currentTimeMillis();
         Date currentDate = new Date(currentTimeMillis);
         Date expirationDate = new Date(currentTimeMillis + TOKEN_EXPIRATION);

         //  存放自定义属性,比如用户拥有的权限
         Map<String, Object> claims = new HashMap<>();

         return Jwts.builder()
                 .setClaims(claims)
                 .setSubject(subject)
                 .setIssuedAt(currentDate)
                 .setExpiration(expirationDate)
                 .signWith(SignatureAlgorithm.HS512, TOKEN_SECRET_KEY)
                 .compact();
     }

     public static String extractUsername(String token) {
         return extractClaim(token, Claims::getSubject);
     }

     public static boolean isTokenExpired(String token) {
         return extractExpiration(token).before(new Date());
     }

     public static Date extractExpiration(String token) {
         return extractClaim(token, Claims::getExpiration);
     }

     public static <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
         final Claims claims = extractAllClaims(token);
         return claimsResolver.apply(claims);
     }

     private static Claims extractAllClaims(String token) {
         return Jwts.parser().setSigningKey(TOKEN_SECRET_KEY).parseClaimsJws(token).getBody();
     }

 }

前后端约定登录成功以后,将token放到header中。于是,我们需要过滤器来处理请求Header中的token,为此定义一个TokenFilter

package com.example.demo5.filter;

  import com.alibaba.fastjson.JSON;
  import com.example.demo5.domain.MyUserDetails;
  import org.apache.commons.lang3.StringUtils;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.data.redis.core.StringRedisTemplate;
  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;
 import java.util.concurrent.TimeUnit;

 /**
  * @Author ChengJianSheng
  * @Date 2021/6/17
  */
 @Component
 public class TokenFilter extends OncePerRequestFilter {

     @Autowired
     private StringRedisTemplate stringRedisTemplate;

     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
         String token = request.getHeader("token");
         System.out.println("请求头中带的token: " + token);
         String key = "TOKEN:" + token;
         if (StringUtils.isNotBlank(token)) {
             String value = stringRedisTemplate.opsForValue().get(key);
             if (StringUtils.isNotBlank(value)) {
 //                String username = JwtUtils.extractUsername(token);
                 MyUserDetails user = JSON.parseObject(value, MyUserDetails.class);
                 if (null != user && null == SecurityContextHolder.getContext().getAuthentication()) {
                     UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                     SecurityContextHolder.getContext().setAuthentication(authenticationToken);

                     //  刷新token
                     //  如果生存时间小于10分钟,则再续1小时
                     long time = stringRedisTemplate.getExpire(key);
                     if (time < 600) {
                         stringRedisTemplate.expire(key, (time + 3600), TimeUnit.SECONDS);
                     }
                 }
             }
         }

         chain.doFilter(request, response);
     }
 }

token过滤器做了两件事,一是获取header中的token,构造UsernamePasswordAuthenticationToken放入上下文中。权限可以从数据库中再查一遍,也可以直接从之前的缓存中获取。二是为token续期,即刷新token。

由于我们采用jwt生成token,因此没法中途更改token的有效期,只能将其放到Redis中,通过更改Redis中key的生存时间来控制token的有效期。

6. 访问控制

首先来定义资源

package com.example.demo5.controller;

  import org.springframework.security.access.prepost.PreAuthorize;
  import org.springframework.web.bind.annotation.GetMapping;
  import org.springframework.web.bind.annotation.RequestMapping;
  import org.springframework.web.bind.annotation.RestController;

  /**
   * @Author ChengJianSheng
  * @Date 2021/6/12
  */
 @RestController
 @RequestMapping("/hello")
 public class HelloController {

     @PreAuthorize("@myAccessDecisionService.hasPermission('hello:sayHello')")
     @GetMapping("/sayHello")
     public String sayHello() {
         return "hello";
     }

     @PreAuthorize("@myAccessDecisionService.hasPermission('hello:sayHi')")
     @GetMapping("/sayHi")
     public String sayHi() {
         return "hi";
     }
 }

资源的访问控制我们通过判断是否有相应的权限字符串

package com.example.demo5.service;

  import org.springframework.security.core.Authentication;
  import org.springframework.security.core.GrantedAuthority;
  import org.springframework.security.core.authority.SimpleGrantedAuthority;
  import org.springframework.security.core.context.SecurityContextHolder;
  import org.springframework.security.core.userdetails.UserDetails;
  import org.springframework.stereotype.Component;

 import java.util.Set;
 import java.util.stream.Collectors;

 @Component("myAccessDecisionService")
 public class MyAccessDecisionService {

     public boolean hasPermission(String permission) {
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
         Object principal = authentication.getPrincipal();
         if (principal instanceof UserDetails) {
             UserDetails userDetails = (UserDetails) principal;
 //            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
             Set<String> set = userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
             return set.contains(permission);
         }
         return false;
     }
 }

7. 配置WebSecurity

package com.example.demo5.config;

  import com.example.demo5.filter.TokenFilter;
  import com.example.demo5.handler.*;
  import com.example.demo5.service.MyUserDetailsService;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
  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.configuration.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 import org.springframework.security.config.http.SessionCreationPolicy;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

 /**
  * @Author ChengJianSheng
  * @Date 2021/6/12
  */
 @EnableGlobalMethodSecurity(prePostEnabled = true)
 @EnableWebSecurity
 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

     @Autowired
     private MyUserDetailsService myUserDetailsService;
     @Autowired
     private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
     @Autowired
     private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
     @Autowired
     private TokenFilter tokenFilter;

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

     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.formLogin()
 //                .usernameParameter("username")
 //                .passwordParameter("password")
 //                .loginPage("/login.html")
                 .successHandler(myAuthenticationSuccessHandler)
                 .failureHandler(myAuthenticationFailureHandler)
                 .and()
                 .logout().logoutSuccessHandler(new MyLogoutSuccessHandler())
                 .and()
                 .authorizeRequests()
                 .antMatchers("/demo/login").permitAll()
 //                .antMatchers("/css/**", "/js/**", "/**/images/*.*").permitAll()
 //                .regexMatchers(".+[.]jpg").permitAll()
 //                .mvcMatchers("/hello").servletPath("/demo").permitAll()
                 .anyRequest().authenticated()
                 .and()
                 .exceptionHandling()
                 .accessDeniedHandler(new MyAccessDeniedHandler())
                 .authenticationEntryPoint(new MyAuthenticationEntryPoint())
                 .and()
                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                 .maximumSessions(1)
                 .maxSessionsPreventsLogin(false)
                 .expiredSessionStrategy(new MyExpiredSessionStrategy());

         http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);

         http.csrf().disable();
     }

     public PasswordEncoder passwordEncoder() {
         return new BCryptPasswordEncoder();
     }

     public static void main(String[] args) {
         System.out.println(new BCryptPasswordEncoder().encode("123456"));
     }
 }

注意,我们将自定义的TokenFilter放到UsernamePasswordAuthenticationFilter之前

所有过滤器的顺序可以查看 org.springframework.security.config.annotation.web.builders.FilterComparator 或者org.springframework.security.config.annotation.web.builders.FilterOrderRegistration

8. 看效果

9. 补充:手机号+短信验证码登录

参照org.springframework.security.authentication.UsernamePasswordAuthenticationToken写一个短信认证Token

package com.example.demo5.filter;

  import org.springframework.security.authentication.AbstractAuthenticationToken;
  import org.springframework.security.core.GrantedAuthority;
  import org.springframework.security.core.SpringSecurityCoreVersion;
  import org.springframework.util.Assert;

  import java.util.Collection;

 /**
  * @Author ChengJianSheng
  * @Date 2021/5/12
  */
 public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

     private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

     private final Object principal;

     private Object credentials;

     public SmsCodeAuthenticationToken(Object principal, Object credentials) {
         super(null);
         this.principal = principal;
         this.credentials = credentials;
         setAuthenticated(false);
     }

     public SmsCodeAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
         super(authorities);
         this.principal = principal;
         this.credentials = credentials;
         super.setAuthenticated(true);
     }

     @Override
     public Object getCredentials() {
         return credentials;
     }

     @Override
     public Object getPrincipal() {
         return principal;
     }

     @Override
     public void setAuthenticated(boolean authenticated) {
         Assert.isTrue(!authenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
         super.setAuthenticated(false);
     }

     @Override
     public void eraseCredentials() {
         super.eraseCredentials();
     }
 }

参照org.springframework.security.authentication.dao.DaoAuthenticationProvider写一个自己的短信认证Provider

package com.example.demo5.filter;

  import com.example.demo.service.MyUserDetailsService;
  import org.apache.commons.lang3.StringUtils;
  import org.springframework.security.authentication.AuthenticationProvider;
  import org.springframework.security.authentication.BadCredentialsException;
  import org.springframework.security.core.Authentication;
  import org.springframework.security.core.AuthenticationException;
  import org.springframework.security.core.userdetails.UserDetails;

 /**
  * @Author ChengJianSheng
  * @Date 2021/5/12
  */
 public class SmsAuthenticationProvider implements AuthenticationProvider {

     private MyUserDetailsService myUserDetailsService;

     @Override
     public Authentication authenticate(Authentication authentication) throws AuthenticationException {
         //  校验验证码
         additionalAuthenticationChecks((SmsCodeAuthenticationToken) authentication);

         //  校验手机号
         String mobile = authentication.getPrincipal().toString();

         UserDetails userDetails = myUserDetailsService.loadUserByMobile(mobile);

         if (null == userDetails) {
             throw new BadCredentialsException("手机号不存在");
         }

         //  创建认证成功的Authentication对象
         SmsCodeAuthenticationToken result = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
         result.setDetails(authentication.getDetails());

         return result;
     }

     protected void additionalAuthenticationChecks(SmsCodeAuthenticationToken authentication) throws AuthenticationException {
         if (authentication.getCredentials() == null) {
             throw new BadCredentialsException("验证码不能为空");
         }
         String mobile = authentication.getPrincipal().toString();
         String smsCode = authentication.getCredentials().toString();

         //  从Session或者Redis中获取相应的验证码
         String smsCodeInSessionKey = "SMS_CODE_" + mobile;
 //        String verificationCode = sessionStrategy.getAttribute(servletWebRequest, smsCodeInSessionKey);
 //        String verificationCode = stringRedisTemplate.opsForValue().get(smsCodeInSessionKey);
         String verificationCode = "1234";

         if (StringUtils.isBlank(verificationCode)) {
             throw new BadCredentialsException("短信验证码不存在,请重新发送!");
         }
         if (!smsCode.equalsIgnoreCase(verificationCode)) {
             throw new BadCredentialsException("验证码错误!");
         }

         //todo  清除Session或者Redis中获取相应的验证码
     }

     @Override
     public boolean supports(Class<?> authentication) {
         return (SmsCodeAuthenticationToken.class.isAssignableFrom(authentication));
     }

     public MyUserDetailsService getMyUserDetailsService() {
         return myUserDetailsService;
     }

     public void setMyUserDetailsService(MyUserDetailsService myUserDetailsService) {
         this.myUserDetailsService = myUserDetailsService;
     }
 }

参照org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter写一个短信认证处理的过滤器

package com.example.demo.filter;

  import org.springframework.security.authentication.AuthenticationManager;
  import org.springframework.security.authentication.AuthenticationServiceException;
  import org.springframework.security.core.Authentication;
  import org.springframework.security.core.AuthenticationException;
  import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
  import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;

 /**
  * @Author ChengJianSheng
  * @Date 2021/5/12
  */
 public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

     public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";

     public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "smsCode";

     private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login/mobile", "POST");

     private String usernameParameter = SPRING_SECURITY_FORM_MOBILE_KEY;

     private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

     private boolean postOnly = true;

     public SmsAuthenticationFilter() {
         super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
     }

     public SmsAuthenticationFilter(AuthenticationManager authenticationManager) {
         super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
     }

     @Override
     public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
         if (postOnly && !request.getMethod().equals("POST")) {
             throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
         }

         String mobile = obtainMobile(request);
         mobile = (mobile != null) ? mobile : "";
         mobile = mobile.trim();
         String smsCode = obtainPassword(request);
         smsCode = (smsCode != null) ? smsCode : "";

         SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile, smsCode);

         setDetails(request, authRequest);

         return this.getAuthenticationManager().authenticate(authRequest);
     }

     private String obtainMobile(HttpServletRequest request) {
         return request.getParameter(this.usernameParameter);
     }

     private String obtainPassword(HttpServletRequest request) {
         return request.getParameter(this.passwordParameter);
     }

     protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
         authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
     }
 }

在WebSecurity中进行配置

package com.example.demo.config;

  import com.example.demo.filter.SmsAuthenticationFilter;
  import com.example.demo.filter.SmsAuthenticationProvider;
  import com.example.demo.handler.MyAuthenticationFailureHandler;
  import com.example.demo.handler.MyAuthenticationSuccessHandler;
  import com.example.demo.service.MyUserDetailsService;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.web.DefaultSecurityFilterChain;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 import org.springframework.stereotype.Component;

 /**
  * @Author ChengJianSheng
  * @Date 2021/5/12
  */
 @Component
 public class SmsAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

     @Autowired
     private MyUserDetailsService myUserDetailsService;
     @Autowired
     private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
     @Autowired
     private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

     @Override
     public void configure(HttpSecurity http) throws Exception {
         SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
         smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
         smsAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
         smsAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);

         SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
         smsAuthenticationProvider.setMyUserDetailsService(myUserDetailsService);

         http.authenticationProvider(smsAuthenticationProvider)
                 .addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
     }
 }
 http.apply(smsAuthenticationConfig);

以上就是基于 Spring Security前后端分离的权限控制系统的详细内容,更多关于Spring Security权限控制系统的资料请关注我们其它相关文章!

(0)

相关推荐

  • SpringBoot+Vue前后端分离,使用SpringSecurity完美处理权限问题的解决方法

    当前后端分离时,权限问题的处理也和我们传统的处理方式有一点差异.笔者前几天刚好在负责一个项目的权限管理模块,现在权限管理模块已经做完了,我想通过5-6篇文章,来介绍一下项目中遇到的问题以及我的解决方案,希望这个系列能够给小伙伴一些帮助.本系列文章并不是手把手的教程,主要介绍了核心思路并讲解了核心代码,完整的代码小伙伴们可以在GitHub上star并clone下来研究.另外,原本计划把项目跑起来放到网上供小伙伴们查看,但是之前买服务器为了省钱,内存只有512M,两个应用跑不起来(已经有一个V部落开

  • Vue 动态路由的实现及 Springsecurity 按钮级别的权限控制

    思路 : 动态路由实现:在导航守卫中判断用户是否有用户信息, 通过调用接口,拿到后台根据用户角色生成的菜单树, 格式化菜单树结构信息并递归生成层级路由表并 使用Vuex保存,通过  router.addRoutes  动态挂载到  router  上,按钮级别的权限控制,则需使用自定义指令去实现. 实现: 导航守卫代码: router.beforeEach((to, from, next) => { NProgress.start() // start progress bar to.meta

  • 详解Spring Security 中的四种权限控制方式

    Spring Security 中对于权限控制默认已经提供了很多了,但是,一个优秀的框架必须具备良好的扩展性,恰好,Spring Security 的扩展性就非常棒,我们既可以使用 Spring Security 提供的方式做授权,也可以自定义授权逻辑.一句话,你想怎么玩都可以! 今天松哥来和大家介绍一下 Spring Security 中四种常见的权限控制方式. 表达式控制 URL 路径权限 表达式控制方法权限 使用过滤注解 动态权限 四种方式,我们分别来看.  1.表达式控制 URL 路径权

  • java中自定义Spring Security权限控制管理示例(实战篇)

    背景描述 项目中需要做细粒的权限控制,细微至url + httpmethod (满足restful,例如: https://.../xxx/users/1, 某些角色只能查看(HTTP GET), 而无权进行增改删(POST, PUT, DELETE)). 表设计 为避嫌,只列出要用到的关键字段,其余敬请自行脑补. 1.admin_user 管理员用户表, 关键字段( id, role_id ). 2.t_role 角色表, 关键字段( id, privilege_id ). 3.t_privi

  • SpringBoot Security前后端分离登录验证的实现

    最近一段时间在研究OAuth2的使用,想整个单点登录,从网上找了很多demo都没有实施成功,也许是因为压根就不懂OAuth2的原理导致.有那么几天,越来越没有头绪,又不能放弃,转过头来一想,OAuth2是在Security的基础上扩展的,对于Security自己都是一无所知,干脆就先研究一下Security吧,先把Security搭建起来,找找感觉. 说干就干,在现成的SpringBoot 2.1.4.RELEASE环境下,进行Security的使用. 简单的Security的使用就不说了,目前

  • Spring Security如何使用URL地址进行权限控制

    这篇文章主要介绍了Spring Security如何使用URL地址进行权限控制,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 目的是:系统内存在很多不同的用户,每个用户具有不同的资源访问权限,具体表现就是某个用户对于某个URL是无权限访问的.需要Spring Security忙我们过滤. FilterSecurityInterceptor是Spring Security进行URL权限判断的,FilterSecurityInterceptor又继

  • SpringSecurity+JWT实现前后端分离的使用详解

    创建一个配置类 SecurityConfig 继承 WebSecurityConfigurerAdapter package top.ryzeyang.demo.common.config; import org.springframework.context.annotation.Bean; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework

  • SpringBoot+Spring Security+JWT实现RESTful Api权限控制的方法

    摘要:用spring-boot开发RESTful API非常的方便,在生产环境中,对发布的API增加授权保护是非常必要的.现在我们来看如何利用JWT技术为API增加授权保护,保证只有获得授权的用户才能够访问API. 一:开发一个简单的API 在IDEA开发工具中新建一个maven工程,添加对应的依赖如下: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-b

  • 基于Spring Security前后端分离的权限控制系统问题

    目录 1. 引入maven依赖 2. 建表并生成相应的实体类 3. 自定义UserDetails 4. 自定义各种Handler 5. Token处理 6. 访问控制 7. 配置WebSecurity 8. 看效果 9. 补充:手机号+短信验证码登录 前后端分离的项目,前端有菜单(menu),后端有API(backendApi),一个menu对应的页面有N个API接口来支持,本文介绍如何基于Spring Security前后端分离的权限控制系统问题. 话不多说,入正题.一个简单的权限控制系统需要

  • 基于NodeJS的前后端分离的思考与实践(五)多终端适配

    前言 近年来各站点基于 Web 的多终端适配进行得如火如荼,行业间也发展出依赖各种技术的解决方案.有如基于浏览器原生 CSS3 Media Query 的响应式设计.基于云端智能重排的「云适配」方案等.本文则主要探讨在前后端分离基础下的多终端适配方案. 关于前后端分离 关于前后端分离的方案,在<基于NodeJS的前后端分离的思考与实践(一)>中有非常清晰的解释.我们在服务端接口和浏览器之间引入 NodeJS 作为渲染层,因为 NodeJS 层彻底与数据抽离,同时无需关心大量的业务逻辑,所以十分

  • 基于NodeJS的前后端分离的思考与实践(一)全栈式开发

    前言 为了解决传统Web开发模式带来的各种问题,我们进行了许多尝试,但由于前/后端的物理鸿沟,尝试的方案都大同小异.痛定思痛,今天我们重新思考了"前后端"的定义,引入前端同学都熟悉的NodeJS,试图探索一条全新的前后端分离模式. 随着不同终端(Pad/Mobile/PC)的兴起,对开发人员的要求越来越高,纯浏览器端的响应式已经不能满足用户体验的高要求,我们往往需要针对不同的终端开发定制的版本.为了提升开发效率,前后端分离的需求越来越被重视,后端负责业务/数据接口,前端负责展现/交互逻

  • Springboot+Vue+shiro实现前后端分离、权限控制的示例代码

    本文总结自实习中对项目的重构.原先项目采用Springboot+freemarker模版,开发过程中觉得前端逻辑写的实在恶心,后端Controller层还必须返回Freemarker模版的ModelAndView,逐渐有了前后端分离的想法,由于之前,没有接触过,主要参考的还是网上的一些博客教程等,初步完成了前后端分离,在此记录以备查阅. 一.前后端分离思想 前端从后端剥离,形成一个前端工程,前端只利用Json来和后端进行交互,后端不返回页面,只返回Json数据.前后端之间完全通过public A

  • Java前后端分离之权限管理示例分析

    目录 1.前端界面 1.1 按钮 1.2 对话框+树形控件 2.后端操作 2.1 controller层 2.2 serviceImpl层 2.3 结果展示 2.4 查对应的权限菜单(使用中间表) 2.4.1 后端处理(权限回显) 2.4.2 前端处理 2.4.3 后端处理(确定修改权限) 1.前端界面 1.1 按钮 借助elementui中的表格部分 <template slot-scope="scope"> <el-button @click="perm

  • 基于NodeJS的前后端分离的思考与实践(四)安全问题解决方案

    前言 在前后端分离的开发模式中,从开发的角色和职能上来讲,一个最明显的变化就是:以往传统中,只负责浏览器环境中开发的前端同学,需要涉猎到服务端层面,编写服务端代码.而摆在面前的一个基础性问题就是如何保障Web安全? 本文就在前后端分离模式的架构下,针对前端在Web开发中,所遇到的安全问题以及应对措施和注意事项,并提出解决方案. 跨站脚本攻击(XSS)的防御 问题及解决思路 跨站脚本攻击(XSS,Cross-site scripting)是最常见和基本的攻击Web网站的方法.攻击者可以在网页上发布

  • 基于NodeJS的前后端分离的思考与实践(六)Nginx + Node.js + Java 的软件栈部署实践

    淘宝网线上应用的传统软件栈结构为 Nginx + Velocity + Java,即: 在这个体系中,Nginx 将请求转发给 Java 应用,后者处理完事务,再将数据用 Velocity 模板渲染成最终的页面. 引入 Node.js 之后,我们势必要面临以下几个问题: 技术栈的拓扑结构该如何设计,部署方式该如何选择,才算是科学合理?项目完成后,该如何切分流量,对运维来说才算是方便快捷?遇到线上的问题,如何最快地解除险情,避免更大的损失?如何确保应用的健康情况,在负载均衡调度的层面加以管理?承系

  • 基于NodeJS的前后端分离的思考与实践(三)轻量级的接口配置建模框架

    前言 使用Node做前后端分离的开发模式带来了一些性能及开发流程上的优势, 但同时也面临不少挑战.在淘宝复杂的业务及技术架构下,后端必须依赖Java搭建基础架构,同时提供相关业务接口供前端使用.Node在整个环境中最重要的工作之一就是代理这些业务接口,以方便前端(Node端和浏览器端)整合数据做页面渲染.如何做好代理工作,使得前后端开发分离之后,仍然可以在流程上无缝衔接,是我们需要考虑的问题.本文将就该问题做相关探讨,并提出解决方案. 由于后端提供的接口方式可能多种多样,同时开发人员在编写Nod

  • 基于NodeJS的前后端分离的思考与实践(二)模版探索

    前言 在做前后端分离时,第一个关注到的问题就是 渲染,也就是 View 这个层面的工作. 在传统的开发模式中,浏览器端与服务器端是由不同的前后端两个团队开发,但是模版却又在这两者中间的模糊地带.因此模版上面总不可避免的越来越多复杂逻辑,最终难以维护. 而我们选择了NodeJS,作为一个前后端的中间层.试图藉由NodeJS,来疏理 View 层面的工作. 使得前后端分工更明确,让专案更好维护,达成更好的用户体验. 本文 渲染这块工作,对于前端开发者的日常工作来说,佔了非常大的比例,也是最容易与后端

随机推荐