入门demo(三)身份认证、授权和拦截

一、自定义身份认证和授权流程:

1、身份认证

关于登陆:因sercurity默认登陆接口为 /login,而具体的认证逻辑走完已经实现了登陆(获取了令牌),所以系统中无需再定义登陆controller接口。

从前面的demo中可以看到,当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。如果需要自定义逻辑时,只需要实现 UserDetailsService接口。这样当用户登录是先进入认证过滤器获得用户的账号和密码然后到userDetailService实现类查询用户信息和用户权限之后返回security对象就是userDetail。但是我们需要收集用户名和密码传到UserDetailsService服务中,可以通过继承UsernamePasswordAuthenticationFilter 实现表单参数收集的功能,sercurity默认该过滤器只拦截post请求的 /login 接口。

具体认证流程为:

1.1、获取表单提交参数:

主要是收集用户名、密码。通过继承UsernamePasswordAuthenticationFilter,在attemptAuthentication方法中完成参数收集,收集后调用authenticationManager的authenticate方法完成认证。

1.2、身份认证逻辑

在authenticationManager中,而authenticationManager内部实现调用了UserDetailsService的loadUserByUsername。这一步无需代码实现。

1.3、认证服务

UserDetailsService.loadUserByUsername,返回UserDetails。通过实现UserDetailsService接口来覆写loadUserByUsername方法实现。

1.4、认证成功

UsernamePasswordAuthenticationFilter.successfulAuthentication接收UserDetails,生成自定义token并返回;同时可以将token缓存到redis中,用于后面权限拦截判断token是否过期的安全校验、以及退出功能的实现(删除redis的用户登陆key,这样权限拦截会判断token失效,实现账号退出的功能)。

2、授权

在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验,

 在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息,判断当前用户是否拥有访问当前资源所需的权限。

UsernamePasswordAuthenticationFilter身份认证成功后,其他非 /login接口是否是合法请求?

可以通过继承BasicAuthenticationFilter,在doFilterInternal中做处理:

2.1、判断用户是否合法

从请求中获取token,判断token是否为空、是否过期、是否正确(和token的生成方式一致),校验不通过认为用户不合法,return 返回错误信息。

2.2、合法请求

(1)根据token获取用户id,再根据用户id从数据库中查询资源、角色等(系统常用的)信息,放到SecurityContextHolder全局变量中。

UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(currentUser,null,currentUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authentication);

后续系统中需要获取当前登陆用户的信息,也直接从 SecurityContextHolder中获取

(UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

当然仅限于配置了该过滤器的接口,对于WebSecurityConfigurerAdapter中配置的白名单接口未设置用户信息自然也取不到。 

(2)最后放行,chain.doFilter(req, res);

二、身份认证和授权demo:

下面以一个demo来演示下:

1、pom和application配置文件
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.demo.security</groupId>
    <artifactId>security-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.14.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.2</version>
        </dependency>

        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.7.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.6</version>
        </dependency>
        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    
</project>
server.port=8089
server.servlet.context-path=/securityDemo

my.redis.server.host= localhost
my.redis.server.port=6379
my.redis.server.jedis.pool.maxTotal=500
my.redis.server.jedis.pool.maxIdle=10
my.redis.server.jedis.pool.maxWaitMillis=5000
my.redis.server.jedis.pool.min-idle=5
my.redis.server.timeout=5000
2、sercurity总配置类
import com.security.demo.filter.AuthenticationFilter;
import com.security.demo.filter.LoginFilter;
import com.security.demo.util.RedisClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private RedisClient redisClient;

    /**
     * 需要放行的URL
     */
    private static final String[] AUTH_WHITELIST = {
            "/static/**",
            "/index.html",
            "/test/test"
    };

    /**
     * 设置 HTTP 验证规则
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.formLogin().and().authorizeRequests().anyRequest().authenticated().and().csrf().disable()// 所有请求需要身份认证
                .exceptionHandling()
                .and()
                //1、登陆、退出url,均由前端拦截器控制,这里注释掉。
                //1.1、前端拦截器中判断缓存token为空,为空则post请求访问/login,目的是进入LoginFilter获取token
                //1.2、不为空则带token访问接口,如果AuthenticationFilter拦截token不合法则根据错误码跳转到登陆页面,重复1.1的操作
                //.logout().logoutUrl("/logout").and()
                //2、身份认证filter,访问系统(除了白名单接口)需要先登陆。post请求/login接口会进入这个拦截器
                // 校验用户名密码是否正确,正确返回token给前端,不正确则返回异常信息
                .addFilterBefore(new LoginFilter(authenticationManager(),redisClient),LoginFilter.class)
                //3、授权filer,authenticationManager为BasicAuthenticationFilter的必传参数。所有的接口都会走到这里
                // 根据用户id查询权限,连同身份一起塞入SecurityContextHolder全局变量,后面获取用户信息则直接从SecurityContextHolder中get
                .addFilterBefore(new AuthenticationFilter(authenticationManager(),redisClient,userDetailsService),AuthenticationFilter.class);
    }


    /**
     * 身份认证
     * @param auth
     * @throws Exception
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(new MyPasswordEncoder());
    }


    /**
     * 配置哪些请求不拦截
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(AUTH_WHITELIST);
    }
}
import org.springframework.security.crypto.password.PasswordEncoder;

public class MyPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence charSequence) {
        return charSequence.toString();
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return s.equals(charSequence.toString());
    }

    /*public DefaultPasswordEncoder() {
        this(-1);
    }
    *//**
     * @param strength
     *            the log rounds to use, between 4 and 31
     *//*
    public DefaultPasswordEncoder(int strength) {
    }
    public String encode(CharSequence rawPassword) {
        return MD5.encrypt(rawPassword.toString());
    }
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
    }*/
}
 3、自定义用户服务UserDetailsService
import com.security.demo.constant.UserConstants;
import com.security.demo.dto.UserDTO;
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 org.springframework.util.StringUtils;

import java.util.List;
import java.util.Map;

/**
 * 当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。
 * 而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑,
 */
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //模拟数据库查询
        Map<String, String> userMap = UserConstants.getUsers();
        String dbPwd = userMap.get(username);
        if(StringUtils.isEmpty(dbPwd)){
            throw new UsernameNotFoundException("用户不存在");
        }
        Map<String, List<String>> userRoles = UserConstants.getUserRoles();
        List<String> roles = userRoles.get(username);
        Map<String, List<String>> userMenus = UserConstants.getUserPermissions();
        List<String> menus = userMenus.get(username);
        UserDTO userDTO = new UserDTO(null,null,username,roles,menus,dbPwd);
        return userDTO;
    }
}

实体:

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

public class UserDTO implements UserDetails {

    private Integer id;
    private String userName;
    private String userAccount;
    private List<String> roles;
    private List<String> menus;
    private String passWord;

    public UserDTO (Integer id,String userName,String userAccount,List<String> roles,List<String> menus,String passWord){
        this.id = id;
        this.userAccount = userAccount;
        this.userName = userName;
        this.roles = roles;
        this.menus = menus;
        this.passWord = passWord;
    }

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

    @Override
    public String getPassword() {
        return passWord;
    }

    @Override
    public String getUsername() {
        return this.userAccount;
    }

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

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

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

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

    public List<String> getRoles() {
        return roles;
    }

    public List<String> getMenus() {
        return menus;
    }
}
 4、拦截器
(1)登陆拦截器LoginFilter
import com.security.demo.constant.UserConstants;
import com.security.demo.dto.UserDTO;
import com.security.demo.util.JwtUtil;
import com.security.demo.util.RedisClient;
import org.apache.http.entity.ContentType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.StringUtils;

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.ArrayList;
import java.util.Map;

/**
 * 登陆拦截器,身份认证
 */
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;
    private RedisClient redisClient;

    public LoginFilter(AuthenticationManager authenticationManager,RedisClient redisClient) {
        this.authenticationManager = authenticationManager;
        this.redisClient = redisClient;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException {
        try {
            String userName = req.getParameter("userName");
            String passWord = req.getParameter("passWord");
            if (StringUtils.isEmpty(userName)) {
                throw new UsernameNotFoundException("请输入账号");
            }
            //验证用户名密码是否正确
            Map<String, String> userMap = UserConstants.getUsers();
           if(!userMap.keySet().contains(userName)){
               throw new UsernameNotFoundException("用户不存在");
           }
           if(!passWord.equals(userMap.get(userName))){
               throw new UsernameNotFoundException("密码错误");
           }
           //这里权限返回空,由后面的授权过滤器查询
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(userName, passWord, new ArrayList<>()));
        } catch (UsernameNotFoundException e) {
            //返回错误信息
            res.setCharacterEncoding("UTF-8");
            res.setContentType("application/text;charset=utf-8");
            try {
                res.getWriter().write(e.getMessage());
            } catch (IOException e1) {
                e1.printStackTrace();
            }
            return null;
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }
    /**
     * 登录成功(即UserDetailsService正常返回)调用的方法,
     * 这里返回token给前端
     * @param req
     * @param res
     * @param chain
     * @param auth
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        UserDTO userDTO = (UserDTO) auth.getPrincipal();
        String userName = userDTO.getUsername();
        String password = userDTO.getPassword();
        String jwtToken = JwtUtil.sign(userName, password);
        //缓存到redis中
        redisClient.set(userName,jwtToken);
        //返回
        res.setContentType(ContentType.TEXT_HTML.toString());
        res.getWriter().write(jwtToken);
    }
}
(2)token拦截器AuthenticationFilter
import com.security.demo.util.JwtUtil;
import com.security.demo.util.RedisClient;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 登陆成功后的filter
 *从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。
 * 如果校验通过,就认为这是一个取得授权的合法请求
 */
public class AuthenticationFilter extends BasicAuthenticationFilter {

    private RedisClient redisClient;

    private UserDetailsService userDetailsService;

    public AuthenticationFilter(AuthenticationManager authenticationManager,RedisClient redisClient,UserDetailsService userDetailsService) {
        super(authenticationManager);
        this.redisClient = redisClient;
        this.userDetailsService = userDetailsService;
    }

    //非登陆接口,只携带token
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        logger.info("登陆成功后访问,url={}"+req.getRequestURI());
        String token = req.getHeader("token");
        res.setCharacterEncoding("UTF-8");
        res.setContentType("application/text;charset=utf-8");
        if(StringUtils.isEmpty(token)){
            logger.info("登陆成功后访问,url={},token为空"+req.getRequestURI());
            res.getWriter().write("token为空");
            return;
        }
        //1、token是否正确,是否能反解析出userName
        String userName = JwtUtil.getUsername(token);
        if(StringUtils.isEmpty(userName)){
            logger.info("登陆成功后访问,url={},token错误"+req.getRequestURI());
            res.getWriter().write("token错误");
            return;
        }
        //2、验证token是否过期
        String redisToken = redisClient.get(userName);
        if(StringUtils.isEmpty(redisToken) || !token.equals(redisToken)){
            logger.info("登陆成功后访问,url={},token过期"+req.getRequestURI());
            res.getWriter().write("token过期");
            return;
        }
        UserDetails currentUser = userDetailsService.loadUserByUsername(userName);
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(currentUser,null,currentUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(req, res);
    }
}
5、业务逻辑
(1)常量(模拟数据库查询)
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 模拟数据库查询数据,假设有:用户名/密码/角色/资源
 * admin/123/xtgly/user_manage、role_manage、menu_manage、school_manage
 * zs/123/userAdmin、roleAdmin/user_manage、role_manage、menu_manage
 * ls/123/schoolAdmin/school_manage
 */
public class UserConstants {

    //redis缓存过期时间
    public static final int EXPIRE_TIME = 30 * 60 * 1000;

    public static Map<String, String> getUsers() {
        Map<String,String> users = new HashMap<>();
        users.put("admin","123");
        users.put("zs","123");
        users.put("ls","123");
        return users;
    }

    public static Map<String,List<String>> getUserRoles() {
        Map<String,List<String>> userRoles = new HashMap<>();
        //admin
        List<String> adminRoles = new ArrayList<>();
        adminRoles.add("xtgly");
        userRoles.put("admin",adminRoles);
        //zs
        List<String> zsRoles = new ArrayList<>();
        zsRoles.add("userAdmin");
        zsRoles.add("roleAdmin");
        userRoles.put("zs",zsRoles);
        //ls
        List<String> lsRoles = new ArrayList<>();
        lsRoles.add("schoolAdmin");
        userRoles.put("ls",lsRoles);
        return userRoles;
    }

    public static Map<String,List<String>> getUserPermissions() {
        Map<String,List<String>> userPermissions = new HashMap<>();
        List<String> lsPermissions = new ArrayList<>();
        //ls
        lsPermissions.add("school_manage");
        userPermissions.put("ls",lsPermissions);
        //zs
        List<String> zsPermissions = new ArrayList<>();
        zsPermissions.add("user_manage");
        zsPermissions.add("role_manage");
        zsPermissions.add("menu_manage");
        userPermissions.put("zs",zsPermissions);
        //admin
        List<String> adminPermissions = new ArrayList<>();
        adminPermissions.add("school_manage");
        userPermissions.put("admin",adminPermissions);
        return userPermissions;
    }
}
(2)controller接口
@RequestMapping("/user")
@RestController
public class UserController {

    @Autowired
    private RedisClient redisClient;

    @RequestMapping("/test")
    public String test(){
        return "这是user test";
    }

    @RequestMapping("/getCurrentUser")
    public UserDTO getCurrentUser() {
        UserDTO currentUser = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        System.out.println("当前用户为:"+currentUser);
        return currentUser;
    }

    //退出登陆
    @RequestMapping("/logOut")
    public void logOut(HttpServletRequest request){
        UserDTO currentUser = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String userAccount = currentUser.getUsername();
        redisClient.del(userAccount);
    }
}
@RequestMapping("/school")
@RestController
public class SchoolManageController {
    @RequestMapping("/test")
    public String test(){
        return "这是学校管理";
    }
}
6、测试:
(1)登陆

(2)获取当前登陆用户

(3)访问其他接口

(4)退出登陆

(5)用退出的token再次访问其他接口

  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
知识图谱入门demo是指针对知识图谱技术的初学者制作的演示项目或示例,旨在帮助用户了解知识图谱的基本概念和实际应用。 一个通常的知识图谱入门demo可能包括以下内容: 1. 数据采集与清洗:首先,需要选择一个特定的主题或领域,收集与该领域相关的数据。这些数据可以来自于公开数据库、互联网上的文本或结构化数据等多种来源。然后进行数据清洗,通过文本分析、实体抽取等技术将原始数据转化为结构化的知识表示形式。 2. 知识表示与建模:在demo中,可以选择常用的知识图谱表示方法,如RDF(资源描述框架)或OWL(Web本体语言),将收集到的数据转换为图谱的节点和边的形式。节点代表实体或概念,边表示实体或概念之间的关系。 3. 知识图谱的查询与推理:为了展示知识图谱的查询与推理能力,可以设计一些基本的问题或查询,如通过图谱找到特定实体的属性信息、通过关联关系找到相关的实体等。同时,也可以利用推理算法,发现隐藏在知识图谱中的隐含知识,提供更加丰富的查询结果。 4. 可视化与交互界面:为了更好地展示和使用知识图谱,一个入门demo通常会包含一个用户界面,通过可视化的方式展示知识图谱的结构和内容,并提供查询和筛选等交互操作。 通过参与知识图谱入门demo,用户可以在实践中了解知识图谱的基本原理、构建过程和实际应用。同时,也可以通过与其他学习者和专业人士的交流,进一步深入学习和研究知识图谱技术。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

w_t_y_y

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值