Bootstrap

Spring Security —08—前后端分离开发认证总结案例

8.1前后端分离设计

在这里插入图片描述

8.2 新建Controller进行测试

 package com.study.controller;import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;/**
  * @ClassName TestController
  * @Description TODO
  * @Date 2022/8/9 16:11
  * @Version 1.0
  */
 @RestController
 public class TestController {
     @GetMapping("/test")
     public String test() {
         System.out.println("test is ok!");
         return "Test is Ok!";
     }
 }


启动服务,浏览器访问:http://localhost:8080/test,浏览器输出:Test is Ok!",IDEA控制台输出:test is ok!,表示项目创建成功!

8.3 pom.xml引入依赖

 <!--引入Spring Security依赖-->
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-security</artifactId>
 </dependency>
  <!--引入数据源-->
 <dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>druid</artifactId>
     <version>1.2.7</version>
 </dependency><!--引入mysql-->
 <dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
     <version>5.1.38</version>
 </dependency><!--引入mybatis-->
 <dependency>
     <groupId>org.mybatis.spring.boot</groupId>
     <artifactId>mybatis-spring-boot-starter</artifactId>
     <version>2.2.0</version>
 </dependency>

刷新maven,重启启动服务,浏览器再次访问:http://localhost:8080/test,此时需要进行登录认证,用户名为:user,密码为引入SpringSecurity框架后IDEA控制台产生的随机密码,输入正确的用户名和密码登录成功后,浏览器输出:Test is Ok!",IDEA控制台输出:test is ok!,表示项目创建成功!

8.4application.properties添加配置

 # datasource:类型、驱动名、用户名、密码
 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
 spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=UTF-8&useSSL=false
 spring.datasource.username=root
 spring.datasource.password=root
 # mybatis配置mapper文件的位置和别名设置
 # 注意mapper目录(包)新建时必须使用"/",而不是.
 mybatis.mapper-locations=classpath:com/study/mapper/*.xml
 mybatis.type-aliases-package=com.study.entity
 # log:为了显示mybatis运行SQL语句
 logging.level.com.study=debug

8.5创建实体类entity

User

 package com.study.entity;import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetails;import java.util.*;/**
  * @ClassName User
  * @Description TODO
  * @Date 2022/8/10 0:25
  * @Version 1.0
  */
 public class User implements UserDetails {
     private Integer id;
     private String username;
     private String password;
     private Boolean enabled;
     private Boolean accountNonExpired;
     private Boolean accountNonLocked;
     private Boolean credentialsNonExpired;
     private List<Role> roles = new ArrayList<>();//关系属性,用来存储当前用户所有角色信息//返回权限信息
     @Override
     public Collection<? extends GrantedAuthority> getAuthorities() {
         Set<SimpleGrantedAuthority> authorities = new HashSet<>();
         roles.forEach(role -> {
             SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
             authorities.add(simpleGrantedAuthority);
         });
         return authorities;
     }@Override
     public String getPassword() {
         return password;
     }public void setPassword(String password) {
         this.password = password;
     }@Override
     public String getUsername() {
         return username;
     }public void setUsername(String username) {
         this.username = username;
     }@Override
     public boolean isAccountNonExpired() {
         return accountNonLocked;
     }@Override
     public boolean isAccountNonLocked() {
         return accountNonLocked;
     }@Override
     public boolean isCredentialsNonExpired() {
         return credentialsNonExpired;
     }@Override
     public boolean isEnabled() {
         return enabled;
     }public Integer getId() {
         return id;
     }public void setId(Integer id) {
         this.id = id;
     }public Boolean getEnabled() {
         return enabled;
     }public void setEnabled(Boolean enabled) {
         this.enabled = enabled;
     }public Boolean getAccountNonExpired() {
         return accountNonExpired;
     }public void setAccountNonExpired(Boolean accountNonExpired) {
         this.accountNonExpired = accountNonExpired;
     }public Boolean getAccountNonLocked() {
         return accountNonLocked;
     }public void setAccountNonLocked(Boolean accountNonLocked) {
         this.accountNonLocked = accountNonLocked;
     }public Boolean getCredentialsNonExpired() {
         return credentialsNonExpired;
     }public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
         this.credentialsNonExpired = credentialsNonExpired;
     }public List<Role> getRoles() {
         return roles;
     }public void setRoles(List<Role> roles) {
         this.roles = roles;
     }
 }

Role

 package com.study.entity;/**
  * @ClassName Role
  * @Description TODO
  * @Date 2022/8/10 0:25
  * @Version 1.0
  */
 public class Role {
     private Integer id;
     private String name;
     private String nameZh;public Integer getId() {
         return id;
     }public void setId(Integer id) {
         this.id = id;
     }public String getName() {
         return name;
     }public void setName(String name) {
         this.name = name;
     }public String getNameZh() {
         return nameZh;
     }public void setNameZh(String nameZh) {
         this.nameZh = nameZh;
     }
 }

8.6创建UserDao

 package com.study.dao;import com.study.entity.Role;
 import com.study.entity.User;
 import org.apache.ibatis.annotations.Mapper;import java.util.List;/**
  * @ClassName UserDao
  * @Description TODO
  * @Date 2022/8/10 0:27
  * @Version 1.0
  */
 @Mapper
 public interface UserDao {
     /**
      * 根据用户名查找用户
      *
      * @param username
      * @return
      */
     User loadUserByUsername(String username);/**
      * 根据用户id查询一个角色,注意一个用户可能不止一种角色
      *
      * @param uid
      * @return
      */
     List<Role> getRolesByUid(Integer uid);
 }

8.7创建UserDaoMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper
         PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><mapper namespace="com.study.dao.UserDao">
     <!--loadUserByUsername-->
     <select id="loadUserByUsername" resultType="User">
         select id,
                username,
                password,
                enabled,
                accountNonExpired,
                accountNonLocked,
                credentialsNonExpired
         from user
         where username = #{username}
     </select><!--getRolesByUid
         需要将角色表和用户-角色表进行关联查询,查询条件为role.id=user_role.uid
         其中,uid是外界传入的参数
     -->
     <select id="getRolesByUid" resultType="Role">
         select r.id,
                r.name,
                r.name_zh nameZh
         from role r,
              user_role ur
         where r.id = ur.rid
           and ur.uid = #{uid}
     </select>
 </mapper>

8.8创建MyUserDetailsService

package com.study.service;import com.study.dao.UserDao;
 import com.study.entity.User;
 import org.springframework.beans.factory.annotation.Autowired;
 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.ObjectUtils;/**
  * @ClassName MyUserDetailsService
  * @Description TODO
  * @Date 2022/8/10 0:26
  * @Version 1.0
  */
 @Service
 public class MyUserDetailsService implements UserDetailsService {
     private final UserDao userDao;@Autowired
     public MyUserDetailsService(UserDao userDao) {
         this.userDao = userDao;
     }@Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         User user = userDao.loadUserByUsername(username);
         if (ObjectUtils.isEmpty(user))
             throw new UsernameNotFoundException("用户不存在");
         user.setRoles(userDao.getRolesByUid(user.getId()));
         return user;
     }
 }

8.9 编写前后端分离认证Filter

在进行前后端分离认证时,后端接收到的是前端发送过来的post形式的json数据,此时需要在json格式中获取用于登录的用户名和密码,而SpringSecurity中用于对用户名和密码进行拦截的过滤器为UsernamePasswordAuthenticationFilter,此时需要对此Filter进行覆盖,重写attemptAuthentication()方法后得到用户名和密码,从而进行认证。

 package com.study.config;import com.fasterxml.jackson.databind.ObjectMapper;
 import org.springframework.http.MediaType;
 import org.springframework.security.authentication.AuthenticationServiceException;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.util.Map;/**
  * @ClassName LoginFilter
  * @Description 自定义前后端分离Filter
  * @Date 2022/8/9 17:42
  * @Version 1.0
  */
 public class LoginFilter extends UsernamePasswordAuthenticationFilter {
     @Override
     public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
         System.out.println("========================================");
         //1.判断是否是 post 方式请求
         if (!request.getMethod().equals("POST")) {
             throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
         }
         //2.判断是否是 json 格式请求类型
         if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
             //3.从 json 数据中获取用户输入用户名和密码进行认证 {"uname":"xxx","password":"xxx"}
             try {
                 Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                 //父类UsernamePasswordAuthenticationFilter有两个属性usernameParameter和passwordParameter
                 //在使用构造器注入LoginFilter时可以将自定义两个参数名称
                 String username = userInfo.get(getUsernameParameter());
                 String password = userInfo.get(getPasswordParameter());
                 System.out.println("用户名: " + username + " 密码: " + password);
                 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                 setDetails(request, authRequest);
                 //使用父类的AuthenticationManager的authenticate进行认证
                 return this.getAuthenticationManager().authenticate(authRequest);
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
         //如果请求的内容类型不是JSON格式或请求不是POST方式,则调用父类的attemptAuthentication方法进行认证。
         //也就是UsernamePasswordAuthenticationFilter的attemptAuthentication方法
         return super.attemptAuthentication(request, response);
     }
 }

8.10完善SpringSecurity配置类

package com.study.config;import com.fasterxml.jackson.databind.ObjectMapper;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.authentication.AuthenticationManager;
 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.configuration.WebSecurityConfigurerAdapter;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.provisioning.InMemoryUserDetailsManager;
 import org.springframework.security.web.AuthenticationEntryPoint;
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.OrRequestMatcher;import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;/**
  * @ClassName SecurityConfig
  * @Description SpringSecurity配置类
  * @Date 2022/8/9 17:36
  * @Version 1.0
  */
 @Configuration
 public class SecurityConfig extends WebSecurityConfigurerAdapter {
     //自定义AuthenticationManager
     //自定义数据源,使用@Bean注入,原来默认的失效
     @Bean
     public UserDetailsService userDetailsService() {
         InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
         inMemoryUserDetailsManager.createUser(
                 User.withUsername("root").password("{noop}123456").roles("admin").build());
         return inMemoryUserDetailsManager;
     }//覆盖默认的AuthenticationManager
     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth.userDetailsService(userDetailsService());
     }//从工厂中暴露出来
     @Override
     @Bean
     public AuthenticationManager authenticationManagerBean() throws Exception {
         return super.authenticationManagerBean();
     }//构造器注入,把自定义Filter交给工厂,用以替换UsernamePasswordAuthenticationFilter
     //使用构造器注入,可以在构造方法中完成对filter的设置
     @Bean
     public LoginFilter loginFilter() throws Exception {
         LoginFilter loginFilter = new LoginFilter();
         loginFilter.setFilterProcessesUrl("/doLogin");//指定认证url
         //自定义接收参数名
         loginFilter.setUsernameParameter("uname");//指定接收json用户名key
         loginFilter.setPasswordParameter("pwd");//指定接收json密码key
         loginFilter.setAuthenticationManager(authenticationManagerBean());
         //认证成功处理
         loginFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
             @Override
             public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                 Map<String, Object> result = new HashMap<String, Object>();
                 result.put("msg", "登录成功");//打印登录成功信息
                 //result.put("status", 200);//打印状态码   此处改为setStatus
                 result.put("用户信息", authentication.getPrincipal());//获得身份信息
                 result.put("authentication", authentication);//打印认证信息
                 response.setContentType("application/json;charset=UTF-8");//设置响应类型
                 response.setStatus(HttpStatus.OK.value());//设置登录成功之后的状态
                 String s = new ObjectMapper().writeValueAsString(result);//json格式转字符串
                 response.getWriter().println(s);//打印json格式数据
             }
         });
         //认证失败处理
         loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {
             Map<String, Object> result = new HashMap<String, Object>();
             result.put("msg", "登录失败:" + exception.getMessage());
             //result.put("status", 500);//打印状态码   此处改为setStatus
             response.setContentType("application/json;charset=UTF-8");
             response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());//设置登录失败之后的状态
             String s = new ObjectMapper().writeValueAsString(result);
             response.getWriter().println(s);
         });
         return loginFilter;
     }@Override
     protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests()
                 .anyRequest().authenticated()//所有请求必须认证
                 .and()
                 .formLogin()//form表单认证时默认采用UsernamePasswordAuthenticationFilter进行拦截
                 // 此处使用LoginFilter进行替换
                 .and()
                 .exceptionHandling()
                 .authenticationEntryPoint(new AuthenticationEntryPoint() {
                     @Override
                     public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                         response.setContentType("application/json;charset=UTF-8");
                         response.setStatus(HttpStatus.UNAUTHORIZED.value());
                         response.getWriter().println("请认证之后再去处理!");
                     }
                 })
                 .and()
                 .logout()
                 //.logoutUrl("/logout")
                 .logoutRequestMatcher(new OrRequestMatcher(
                         new AntPathRequestMatcher("/logout", HttpMethod.DELETE.name()),
                         new AntPathRequestMatcher("/logout", HttpMethod.GET.name())
                 ))
                 .logoutSuccessHandler((request, response, authentication) -> {
                     Map<String, Object> result = new HashMap<String, Object>();
                     result.put("msg", "注销成功");//打印登录成功信息
                     //result.put("status", 200);//打印状态码   此处改为setStatus
                     result.put("用户信息", authentication.getPrincipal());//获得身份信息
                     result.put("authentication", authentication);//打印认证信息
                     response.setContentType("application/json;charset=UTF-8");//设置响应类型
                     response.setStatus(HttpStatus.OK.value());//设置登录成功之后的状态
                     String s = new ObjectMapper().writeValueAsString(result);//json格式转字符串
                     response.getWriter().println(s);//打印json格式数据
                 })
                 .and()
                 .csrf().disable();
         /**
          * addFilterAt:用某个filter替换掉过滤器链中的某个filter
          * addFilterBefore():将某个过滤器放在过滤器链中某个filter之前
          * addFilterAfter():将某个过滤器放在过滤器链中某个filter之后
          */
         http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
     }
 }

8.11 启动服务测试

启动服务,打开Postman进行测试:

  • (1)GET http://localhost:8080/test 登录失败

  • (2)POST http://localhost:8080/doLogin

    uname:root,pwd:123

    uname:blr,pwd:123

    uname:admin,pwd:123

    均可成功登录

  • (3)GET或DELETE http://localhost:8080/logout 成功注销

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;