從零學習SpringSecurity

豐極發表於2021-07-19

一、簡介

SpringSecurity是一個功能強大且高度可定製的身份驗證和訪問控制框架,和spring專案整合更加方便。

 spring-security

二、核心功能

  • 認證(Authentication):指的是驗證某個使用者能否訪問該系統。
  • 授權(Authorization):指的是驗證某個使用者是否有許可權執行某個操作。

三、搭建v1.0版本

1、新建一個springboot專案

myspringsecurity

2、新增maven依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

3、新建一個test的controller

package com.zb.myspringsecurity.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/demo")
public class DemoController {

    @RequestMapping("/hello")
    public String hello() {
        return "hello world";
    }
}

4、啟動專案

MyspringsecurityApplication.main();

5、用瀏覽器測試

http://localhost:8080/demo/hello

我們會發現瀏覽器會跳轉到login頁面,如下圖

 login

6、密碼登陸

我們可以在專案啟動日誌裡面找到密碼

2021-07-19 10:58:48.558  INFO 5244 --- [           main] .s.DelegatingFilterProxyRegistrationBean : Mapping filter: 'springSecurityFilterChain' to: [/*]
2021-07-19 10:58:48.558  INFO 5244 --- [           main] o.s.b.w.servlet.ServletRegistrationBean  : Servlet dispatcherServlet mapped to [/]
2021-07-19 10:58:48.684  INFO 5244 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2021-07-19 10:58:48.812  INFO 5244 --- [           main] .s.s.UserDetailsServiceAutoConfiguration : 

Using generated security password: ced4127a-1677-438e-a65b-2ab219137083

2021-07-19 10:58:48.868  INFO 5244 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@40021799, org.springframework.security.web.context.SecurityContextPersistenceFilter@2d7e1102, org.springframework.security.web.header.HeaderWriterFilter@3fbfa96, org.springframework.security.web.csrf.CsrfFilter@61533ae, org.springframework.security.web.authentication.logout.LogoutFilter@4a699efa, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@4482469c, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4917d36b, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@4a1c0752, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@278f8425, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@2adddc06, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@4ebadd3d, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@332f25c8, org.springframework.security.web.session.SessionManagementFilter@466d49f0, org.springframework.security.web.access.ExceptionTranslationFilter@599f571f, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@7004e3d]
2021-07-19 10:58:48.911  INFO 5244 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2021-07-19 10:58:48.914  INFO 5244 --- [           main] c.z.m.MyspringsecurityApplication        : Started MyspringsecurityApplication in 1.458 seconds (JVM running for 2.405)

  • 使用者名稱是:user
  • 密碼(從日誌找到)是:ced4127a-1677-438e-a65b-2ab219137083

登入成功如下圖:

 login

二、進階版v2.0(配置檔案配置使用者密碼)

1、配置檔案裡面寫使用者名稱密碼

剛才的密碼生成在日誌裡面了,實際使用很不方便,可以把密碼使用者名稱固定配置一下

spring.security.user.name=admin
spring.security.user.password=123
  • 重新啟動專案,會發現沒有生成密碼的日誌了
  • 測試用新的使用者名稱密碼沒問題

三、v3.0(java類裡面寫使用者名稱密碼)

1、在java類裡面配置使用者名稱密碼

剛才是寫在配置檔案裡面,我們還可以寫到java類裡面

package com.zb.myspringsecurity.config.security;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
public class ZbWebSecurityConfigurer extends WebSecurityConfigurerAdapter {

 @Bean
 PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
 }

 public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
            .withUser("zhangsan")
            .password(passwordEncoder().encode("123"))
            .roles("ADMIN")
            .and()
            .withUser("lisi")
            .password(passwordEncoder().encode("123"))
            .roles("ADMIN")
            .and()
            .withUser("wangwu")
            .password(passwordEncoder().encode("123"))
            .roles("ADMIN")
            ;
 }
    
}


我們再重啟專案測試一下,發現用三個使用者名稱密碼都沒問題。

四、v4.0(ignore url)

1、修改上面的java類,增加兩個方法

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers(
            //"/**/*.html",
            "/**/*.js",
            "/**/*.css",
            "/**/*.ico",
            "/**/*.jpg",
            "/**/*.png",
            "/test/**" // 忽略test
    );
}

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .httpBasic();

}
  • 我們配置了ignore的url
  • 我們再加一個controller用來測試
package com.zb.myspringsecurity.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {
    
    @RequestMapping("/test")
    public String hello() {
        return "hello test";
    }
}

  • 注意我們上面的ignore裡面有:"/test/**"
  • 也就是說TestController 不會有登入校驗

重啟專案測試一下沒問題

五、v5.0(在資料庫裡面配置使用者名稱密碼)

1、新建mysql庫


create database myspringsecurity CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;

create user securityuser IDENTIFIED by 'securitypass';

grant all privileges on myspringsecurity.* to securityuser@localhost identified by 'securitypass';

flush privileges;

2、新建使用者表和角色表

DROP TABLE IF EXISTS `tb_user`;

CREATE TABLE `tb_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_name` varchar(50) DEFAULT NULL,
`password` varchar(100) DEFAULT NULL,
`mobile` int(11) DEFAULT NULL,
`sex` int(2) DEFAULT NULL,
`email` varchar(50) DEFAULT NULL,
`status` int(2) DEFAULT NULL,
`create_time` DATE DEFAULT NULL,
`create_id` int(11) DEFAULT NULL,
`update_time` date DEFAULT NULL,
`update_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `tb_role`;

CREATE TABLE `tb_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_name` varchar(50) DEFAULT NULL,
`status` int(2) DEFAULT NULL,
`create_time` DATE DEFAULT NULL,
`create_id` int(11) DEFAULT NULL,
`update_time` date DEFAULT NULL,
`update_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `tr_user_role`;

CREATE TABLE `tr_user_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) DEFAULT NULL,
`role_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

3、初始化一些資料

insert into `tb_user` (id, user_name, password) values (1, 'zhangsan', '123');
insert into `tb_user` (id, user_name, password) values (2, 'lisi', '123');
insert into `tb_user` (id, user_name, password) values (3, 'wangwu', '123');

insert into `tb_role` (id, role_name) values (1, '系統管理員');
insert into `tb_role` (id, role_name) values (2, '一般操作員');

insert into `tr_user_role` (id, user_id, role_id) values (1, 1, 1);
insert into `tr_user_role` (id, user_id, role_id) values (2, 1, 2);
insert into `tr_user_role` (id, user_id, role_id) values (3, 2, 2);

4、加入maven依賴

我們這裡用了mybatis-plus。

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.44</version>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.1.2</version>
</dependency>

5、引入mybatis-plus自動生成程式碼的依賴

<!-- mybatis-plus程式碼生成 -->
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.29</version>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.1.2</version>
</dependency>

6、修改程式碼生成類

package com.zb.myspringsecurity.config.mybatis;

import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

import java.util.ArrayList;
import java.util.List;

public class MybatisGenerator {
    
    public static void main(String[] args) {
        AutoGenerator mpg = new AutoGenerator();

        // 全域性配置
        GlobalConfig gc = new GlobalConfig();
        final String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
        gc.setAuthor("system");
        gc.setOpen(false);
        gc.setFileOverride(true);
        gc.setBaseResultMap(true);
        // gc.setSwagger2(true); 實體屬性 Swagger2 註解
        mpg.setGlobalConfig(gc);

        // 資料來源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://127.0.0.1:3306/myspringsecurity?useUnicode=true&useSSL=false&characterEncoding=utf8");
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.jdbc.Driver");
        dsc.setUsername("securityuser");
        dsc.setPassword("securitypass");
        mpg.setDataSource(dsc);

        // 包配置
        PackageConfig pc = new PackageConfig();
        pc.setParent("com.zb.myspringsecurity");
        mpg.setPackageInfo(pc);

        // 自定義配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };

        // 如果模板引擎是 freemarker
        String templatePath = "/templates/mapper.xml.ftl";
        // 如果模板引擎是 velocity
        // String templatePath = "/templates/mapper.xml.vm";

        // 自定義輸出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定義配置會被優先輸出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定義輸出檔名 , 如果你 Entity 設定了前字尾、此處注意 xml 的名稱會跟著發生變化!!
                return projectPath + "/src/main/resources/mapper/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });
        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);

        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();
        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);

        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        // strategy.setSuperEntityClass("com.baomidou.ant.common.BaseEntity");
        strategy.setEntityLombokModel(true);
        strategy.setRestControllerStyle(false);
        // 公共父類
        // strategy.setSuperControllerClass("com.baomidou.ant.common.BaseController");
        // 寫於父類中的公共欄位
        // strategy.setSuperEntityColumns("id");
        strategy.setInclude("tb_user","tb_role","tr_user_role");
        //  strategy.setControllerMappingHyphenStyle(true);
        strategy.setTablePrefix("tb_", "tr_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }
}

  • 執行生成mapper,service和controller
  • mybatis準備好了,可以修改security配置了

7、修改configure方法

原先的:

public void configure(AuthenticationManagerBuilder auth) throws Exception {

    auth.inMemoryAuthentication()
            .withUser("zhangsan")
            .password(passwordEncoder().encode("123"))
            .roles("ADMIN")
            .and()
            .withUser("lisi")
            .password(passwordEncoder().encode("123"))
            .roles("ADMIN")
            .and()
            .withUser(passwordEncoder().encode("123"))
            .password("123")
            .roles("ADMIN")
            ;

}

改成新的:

@Autowired
ZxUserDetailsServiceImpl zxUserDetailsService;

public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(zxUserDetailsService);
}

8、新增ZxUserDetailsServiceImpl類

package com.zb.myspringsecurity.config.security;

import com.zb.myspringsecurity.entity.Role;
import com.zb.myspringsecurity.entity.User;
import com.zb.myspringsecurity.entity.UserRole;
import com.zb.myspringsecurity.service.IUserRoleService;
import com.zb.myspringsecurity.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.List;

@Component
public class ZxUserDetailsServiceImpl implements UserDetailsService {
    
    @Autowired
    PasswordEncoder passwordEncoder;
    @Autowired
    IUserService iUserService;
    @Autowired
    IUserRoleService iUserRoleService;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        /**
         // DEMO:
         
        List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
        authorityList.add(new SimpleGrantedAuthority("Admin"));
        
        ZxUser zxUser = new ZxUser();
        zxUser.setUserName("zhangsanfeng");
        zxUser.setPassword(passwordEncoder.encode("123"));
        zxUser.setAuthorities(authorityList);
        return zxUser;
         
         */
        
        List<User> userList = iUserService.lambdaQuery().eq(User::getUserName, username).list();
        if (CollectionUtils.isEmpty(userList)) {
            throw new UsernameNotFoundException("不存在的使用者");
        }
        User user = userList.get(0);
        ZxUser zxUser = new ZxUser();
        zxUser.setUserName(user.getUserName());
        zxUser.setPassword(passwordEncoder.encode(user.getPassword()));
        zxUser.setId(user.getId());
        
        List<UserRole> userRoleList = iUserRoleService.lambdaQuery().eq(UserRole::getUserId, zxUser.getId()).list();
        List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
        if (!CollectionUtils.isEmpty(userRoleList)) {
            for (UserRole userRole : userRoleList) {
                authorityList.add(new SimpleGrantedAuthority(String.valueOf(userRole.getRoleId())));
            }
        }
        zxUser.setAuthorities(authorityList);
        return zxUser;
    }
}

9、新增自定義user

package com.zb.myspringsecurity.config.security;

import com.zb.myspringsecurity.entity.User;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

@Data
public class ZxUser extends User implements UserDetails {

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

    @Override
    public String getPassword() {
        return super.getPassword();
    }

    @Override
    public String getUsername() {
        return super.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

10、驗證

重啟服務,用資料庫裡面的使用者和密碼驗證沒問題。

六、v6.0(實現前後端分離,token校驗)

1、引入認證管理器 bean

/**
* 認證管理器
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

2、寫一個統一的登入介面

package com.zb.myspringsecurity.controller;

import com.zb.myspringsecurity.config.security.ZxUser;
import com.zb.myspringsecurity.config.security.ZxUserDetailsServiceImpl;
import com.zb.myspringsecurity.config.vo.CommonResponse;
import com.zb.myspringsecurity.config.vo.LoginParamVo;
import com.zb.myspringsecurity.config.vo.TokenVo;
import com.zb.myspringsecurity.service.TokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@Slf4j
@RestController
@RequestMapping("/login")
public class LoginController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Resource
    ZxUserDetailsServiceImpl userDetailsService;
    
    @Resource
    TokenService tokenService;

    @RequestMapping("/login-in")
    public CommonResponse<TokenVo> login(@RequestBody LoginParamVo loginParamVo) {
        try {
            // 1 建立UsernamePasswordAuthenticationToken
            UsernamePasswordAuthenticationToken token
                    = new UsernamePasswordAuthenticationToken(loginParamVo.getUsername(), loginParamVo.getPassword());
            // 2 認證
            Authentication authentication = this.authenticationManager.authenticate(token);
            // 3 儲存認證資訊
            SecurityContextHolder.getContext().setAuthentication(authentication);
            // 4 載入UserDetails
            ZxUser zxUser = this.userDetailsService.loadUserByUsername(loginParamVo.getUsername());
            // 5 生成自定義token
            TokenVo tokenVo = tokenService.createToken(zxUser);
            return CommonResponse.successWithData(tokenVo);
        } catch (Exception e) {
            return CommonResponse.fail(401, e.getMessage());
        }

    }
}

3、tokenservice

package com.zb.myspringsecurity.service;

import com.zb.myspringsecurity.config.vo.TokenVo;
import org.springframework.security.core.userdetails.UserDetails;

public interface TokenService {
    
    TokenVo createToken(UserDetails details);
    
    boolean verifyToken(String token);
    
    String getUserNameByToken(String token);
}

簡單的實現:

package com.zb.myspringsecurity.service.impl;

import com.zb.myspringsecurity.config.vo.TokenVo;
import com.zb.myspringsecurity.service.TokenService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@Service
public class TokenServiceImpl implements TokenService {
    
    // todo 可以存redis, 設定過期時間
    private static final Map<String, String> tokenMap = new HashMap<>();
    
    @Override
    public TokenVo createToken(UserDetails details) {
        String token = UUID.randomUUID().toString();
        tokenMap.put(token, details.getUsername());
        
        TokenVo tokenVo = new TokenVo();
        tokenVo.setToken(token);
        tokenVo.setExpireTime(60*60);
        
        return tokenVo;
    }

    @Override
    public boolean verifyToken(String token) {
        return tokenMap.get(token) != null;
    }

    @Override
    public String getUserNameByToken(String token) {
        return tokenMap.get(token);
    }

}

  • 建立token, 校驗token, 根據token獲取username
  • 存的是token和username的關係

4、修改校驗方式

修改為:SessionCreationPolicy.STATELESS

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
            .csrf().disable()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .httpBasic()
            ;

}

5、增加一個校驗token的filter

@Autowired
ZbTokenAuthenticationFilter zbTokenAuthenticationFilter;

httpSecurity.addFilterBefore(zbTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

filter:

package com.zb.myspringsecurity.config.security.customer;

import com.zb.myspringsecurity.config.security.ZxUser;
import com.zb.myspringsecurity.config.security.ZxUserDetailsServiceImpl;
import com.zb.myspringsecurity.service.IUserRoleService;
import com.zb.myspringsecurity.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Service;
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;

@Service
public class ZbTokenAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    TokenService tokenService;
    @Autowired
    IUserRoleService iUserRoleService;
    @Autowired
    ZxUserDetailsServiceImpl userDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        logger.info("TokenAuthenticationFilter.doFilterInternal start ...");
        String token = request.getHeader("token");

        if (token == null || "".equals(token)) {
            logger.info("token is null , return .");
            filterChain.doFilter(request, response);
            return;
        }

        if (SecurityContextHolder.getContext().getAuthentication() != null) {
            filterChain.doFilter(request, response);
            return;
        }

       boolean result = tokenService.verifyToken(token);
        if (!result) {
            logger.info("ssoService.verifyToken not pass , return .");
            filterChain.doFilter(request, response);
            return;
        }

        ZxUser zxUser = userDetailsService.loadUserByUsername(tokenService.getUserNameByToken(token));

        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                zxUser, null, zxUser.getAuthorities());

        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        
        logger.info("token valid pass , username : " + zxUser.getUsername());
        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(request, response);
    }
}

  • 至此,我們已經把專案改造成前後端分離的了
  • 有資料庫使用者密碼登入
  • 有登入生成token
  • 有校驗url, header必須包含token

七、v7.0(許可權控制)

我們上面已經把spring security的一個核心功能(認證)說完了,下面我們說授權。

1、增加許可權表,關聯表

DROP TABLE IF EXISTS `tb_permission`;

CREATE TABLE `tb_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`en_name` varchar(50) DEFAULT NULL,
`cn_name` varchar(50) DEFAULT NULL,
`create_time` DATE DEFAULT NULL,
`create_id` int(11) DEFAULT NULL,
`update_time` date DEFAULT NULL,
`update_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `tr_role_permission`;

CREATE TABLE `tr_role_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_id` bigint(20) DEFAULT NULL,
`permission_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

2、初始化資料


insert into `tb_permission` (id, en_name, cn_name) values (1, 'system:user:read', '可讀');
insert into `tb_permission` (id, en_name, cn_name) values (2, 'system:user:edit', '可修改');

insert into `tr_role_permission` (id, role_id, permission_id) values (1, 1, 1);
insert into `tr_role_permission` (id, role_id, permission_id) values (2, 1, 2);
insert into `tr_role_permission` (id, role_id, permission_id) values (3, 2, 1);

3、修改ZxUser

增加permissionSet

@Data
public class ZxUser extends User implements UserDetails {

    ...
    
    private Set<String> permissionSet;
    
    ...
}

4、修改loadUserByUsername方法

增加許可權查詢部分:

@Override
public ZxUser loadUserByUsername(String username) throws UsernameNotFoundException {
    
    List<User> userList = iUserService.lambdaQuery().eq(User::getUserName, username).list();
    if (CollectionUtils.isEmpty(userList)) {
        throw new UsernameNotFoundException("不存在的使用者");
    }
    User user = userList.get(0);
    ZxUser zxUser = new ZxUser();
    zxUser.setUserName(user.getUserName());
    zxUser.setPassword(passwordEncoder.encode(user.getPassword()));
    zxUser.setId(user.getId());
    
    List<SimpleGrantedAuthority> authorityList = new ArrayList<>();

    // role
    List<UserRole> userRoleList = iUserRoleService.lambdaQuery().eq(UserRole::getUserId, zxUser.getId()).list();
    if (!CollectionUtils.isEmpty(userRoleList)) {
        for (UserRole userRole : userRoleList) {
            authorityList.add(new SimpleGrantedAuthority(String.valueOf(userRole.getRoleId())));
        }

        // permission
        List<Long> roleIdList = userRoleList.stream().map(UserRole::getRoleId).collect(Collectors.toList());
        List<RolePermission> rolePermissionList = iRolePermissionService.lambdaQuery()
                .in(RolePermission::getRoleId, roleIdList).list();
        if (!CollectionUtils.isEmpty(rolePermissionList)) {
            Collection<Permission> permissionList = iPermissionService
                    .listByIds(rolePermissionList.stream()
                            .map(RolePermission::getPermissionId).collect(Collectors.toList()));
            Set<String> permissionSet = permissionList.stream().map(Permission::getEnName).collect(Collectors.toSet());
            zxUser.setPermissionSet(permissionSet);
        }
    }
    zxUser.setAuthorities(authorityList);
    return zxUser;
}

5、我們實現一個自己的許可權控制類

package com.zb.myspringsecurity.config.security.customer;

import com.zb.myspringsecurity.config.security.ZxUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.Set;

@Slf4j
@Component
public class ZbPermissionEvaluator implements PermissionEvaluator {
    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
        ZxUser user = (ZxUser) authentication.getPrincipal();
        Set<String> permissonSet = user.getPermissionSet();
        if (permission == null) {
            log.info("permission valid not pass , permission is null");
            return false;
        }
        if (permissonSet.contains(permission.toString())) {
            log.info("permission valid pass , permission : {}", permission.toString());
            return true;
        }
        log.info("permission valid not pass , permission : {}", permission.toString());
        return false;
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        return false;
    }
}

  • 很簡單,就是判斷許可權集合存不存在當前許可權

6、配置使之生效

    
@Autowired
ZbPermissionEvaluator zbPermissionEvaluator;

@Bean
public DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler() {
    DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler();
    defaultWebSecurityExpressionHandler.setPermissionEvaluator(zbPermissionEvaluator);
    return defaultWebSecurityExpressionHandler;
}

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
    
    ...

    httpSecurity.authorizeRequests().expressionHandler(defaultWebSecurityExpressionHandler());

    ...

}

7、測試類

package com.zb.myspringsecurity.controller;


import com.zb.myspringsecurity.entity.User;
import com.zb.myspringsecurity.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.List;

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    IUserService iUserService;

    @PreAuthorize("hasPermission('UserController', 'system:user:read')")
    @RequestMapping("/list")
    public List<User> list(HttpServletRequest request) {
        log.info("session id: {}" , request.getSession().getId());
        return iUserService.list();
    }
}

8、啟動服務測試

  • 注意:我們上面的/user/list, 配置了'system:user:read'許可權
  • 仔細去看我們初始化的資料庫資料,會發現wangwu是沒有任何角色的,也沒有任何許可權
  • 所以,wangwu 不能訪問/user/list

先測試zhangsan:

POST http://localhost:8080/login/login-in
Accept: */*
Cache-Control: no-cache
content-type:application/json

{"username":"zhangsan", "password":"123"}

返回:

{
  "data": {
    "token": "e048ba23-7061-43d6-ab35-7c2eb93acda8",
    "expireTime": 3600
  },
  "code": 200,
  "msg": "ok"
}

用這個token去請求/user/list


GET http://localhost:8080/user/list
Accept: application/json
token: e048ba23-7061-43d6-ab35-7c2eb93acda8

返回:

[
  {
    "id": 1,
    "userName": "zhangsan",
    "password": "123",
    "mobile": null,
    "sex": null,
    "email": null,
    "status": null,
    "createTime": null,
    "createId": null,
    "updateTime": null,
    "updateId": null
  },
  {
    "id": 2,
    "userName": "lisi",
    "password": "123",
    "mobile": null,
    "sex": null,
    "email": null,
    "status": null,
    "createTime": null,
    "createId": null,
    "updateTime": null,
    "updateId": null
  },
  {
    "id": 3,
    "userName": "wangwu",
    "password": "123",
    "mobile": null,
    "sex": null,
    "email": null,
    "status": null,
    "createTime": null,
    "createId": null,
    "updateTime": null,
    "updateId": null
  }
]

  • 用同樣的方法測試lisi和wangwu,lisi可以訪問,wangwu不可以,返回如下
{
  "timestamp": "2021-07-19T06:53:38.833+0000",
  "status": 500,
  "error": "Internal Server Error",
  "message": "No message available",
  "path": "/user/list"
}
  • 當然你還可以統一你的異常回覆資訊,可以自行研究
  • 至此,我們的許可權控制也實現了

八、總結

  • 至此,我們的認證和鑑權都說完了
  • 以上只是一種實現,spring security支援自定義擴充套件,還有其它實現方式,可以自己研究
  • 程式碼放到github上了,關注公眾號:豐極,回覆:myspringsecurity獲取

 豐極

歡迎關注微信公眾號:豐極,更多技術學習分享。

相關文章