• Spring-Security前后端分离权限认证


    前后端分离

    一般来说,我们用SpringSecurity默认的话是前后端整在一起的,比如thymeleaf或者Freemarker,SpringSecurity还自带login登录页,还让你配置登出页,错误页。

    但是现在前后端分离才是正道,前后端分离的话,那就需要将返回的页面换成Json格式交给前端处理了

    SpringSecurity默认的是采用Session来判断请求的用户是否登录的,但是不方便分布式的扩展,虽然SpringSecurity也支持采用SpringSession来管理分布式下的用户状态,不过现在分布式的还是无状态的Jwt比较主流。 所以怎么让SpringSecurity变成前后端分离,可以采用Jwt来做认证

    什么是jwt

    Json web token (JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC7519).该token被设计为紧凑且==安全==的,特别适用于==分布式站点的单点登录(SSO)场景==。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

    官网: JSON Web Token Introduction - jwt.io

    jwt的结构

    . 分割   三部分 

    Header

    Header 部分是一个JSON对象,描述JWT的元数据,通常是下面的样子。

    1. {
    2. "alg": "HS256",
    3. "typ": "JWT"
    4. }

    上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256 (写成 HS256) ;typ属性表示这个令牌(token)的类型(type), JWT令牌统一写为JWT。

    最后,将上面的JSON对象使用Base64URL算法转成字符串。

    Payload(载荷)

    Payload 部分也是一个JSON对象,==用来存放实际需要传递的数据==。JWT规定了7个官方字段,供选用。

    iss (issuer):签发人

    exp (expiration time):过期时间

    sub (subject):主题

    aud (audience):受众

    nbf (Not Before):生效时间

    iat (lssued At):签发时间

    jti (JWT ID):编号

    除了官方字段,==你还可以在这个部分定义私有字段==,下面就是一个例子。

    1. {
    2. "sub": "1234567890",
    3. "name" : "John Doe",
    4. “userid”:2
    5. "admin": true
    6. }

    注意,JWT 默认是不加密的,任何人都可以读到,所以不要把==秘密信息==放在这个部分。这个JSON 对象也要使用Base64URL 算法转成字符串。

    Signature

    Signature部分是对前两部分的签名,防止数据篡改。

    首先,需要指定一个==密钥(secret)==。这个密钥只有==服务器才知道==,不能泄露给用户。然后,使用Header里面指定的==签名算法(默认是 HMAC SHA256)==,按照下面的公式产生签名。

    1. HMACSHA256(
    2. base64UrlEncode(header) + ".”"+base64UrlEncode(payload),
    3. secret)

    算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

    1.项目添加hutool依赖

    1. <dependency>
    2. <groupId>cn.hutool</groupId>
    3. <artifactId>hutool-all</artifactId>
    4. <version>5.8.18</version>
    5. </dependency>

    http://t.csdnimg.cn/TA0Xx基于文章中连接数据库的实例基础上进行的前后端分离设计

    2.搭建好一个vue项目

    所需的导入包

    3.修改配置文件 main.js

    全局导入引入

    1. import Vue from 'vue'
    2. import App from './App.vue'
    3. import router from './router'
    4. Vue.config.productionTip = false
    5. import ElementUI from 'element-ui';
    6. import 'element-ui/lib/theme-chalk/index.css';
    7. Vue.use(ElementUI);
    8. import axios from 'axios'
    9. // 后端项目的时候 http://localhost:8080
    10. // axios设置一个默认的路径
    11. // 创建实例时配置默认值
    12. const instance = axios.create({
    13. // 访问路径的时候假的一个基础的路径
    14. baseURL: 'http://localhost:8080/',
    15. // withCredentials: true
    16. });

    请求拦截器与响应拦截器

    1. // 请求拦截器
    2. //
    3. instance.interceptors.request.use( config=> {
    4. // config 前端 访问后端的时候 参数
    5. // 如果sessionStorage里面于token 携带着token 过去
    6. if(sessionStorage.getItem("token")){
    7. // token的值 放到请求头里面
    8. let token = sessionStorage.getItem("token");
    9. config.headers['token']=token;
    10. }
    11. // config.headers['Authorization']="yyl"
    12. return config;
    13. }, error=> {
    14. // 超出 2xx 范围的状态码都会触发该函数。
    15. // 对响应错误做点什么
    16. return Promise.reject(error);
    17. });
    18. // 添加响应拦截器
    19. instance.interceptors.response.use( response=> {
    20. console.log(response)
    21. // 状态码 500
    22. if(response.data.code!=200){
    23. alert("chucuole")
    24. console.log(response.data);
    25. router.push({path:"/login"});
    26. return;
    27. }
    28. return response;
    29. }, error=> {
    30. // 超出 2xx 范围的状态码都会触发该函数。
    31. // 对响应错误做点什么
    32. return Promise.reject(error);
    33. });
    34. Vue.prototype.$axios = instance;
    35. // 引入组件

    挂载点

    1. new Vue({
    2. router,
    3. render: h => h(App)
    4. }).$mount('#app')

    4.搭建一个.vue页面,并在 router 目录下的 index.js 文件配置好路由

    1. <template>
    2. <div class="login-container">
    3. <el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" label-width="100px" class="login-form">
    4. <el-form-item label="用户名" prop="username">
    5. <el-input type="text" v-model="ruleForm.username" autocomplete="off">el-input>
    6. el-form-item>
    7. <el-form-item label="确认密码" prop="password">
    8. <el-input type="password" v-model="ruleForm.password" autocomplete="off">el-input>
    9. el-form-item>
    10. <el-form-item>
    11. <el-button type="primary" @click="submitForm('ruleForm')">提交el-button>
    12. <el-button @click="resetForm('ruleForm')">重置el-button>
    13. el-form-item>
    14. el-form>
    15. div>
    16. template>

    搭建的页面包含基本的登录表单,在新建一个页面用于成功的页面展示,如 图中跳转的main.vue

    1. methods: {
    2. submitForm(formName) {
    3. this.$refs[formName].validate((valid) => {
    4. if (valid) {
    5. alert('submit!');
    6. // 请求 userlogin userlogin
    7. //post i请求 json 数据 后端接受的时候 @RequestBody
    8. this.$axios.post("userlogin",qs.stringify(this.ruleForm)).then(r=>{
    9. // 获取token的值
    10. console.log(r.data.t);
    11. // 存起来
    12. sessionStorage.setItem("token",r.data.t)
    13. // 成功之后 跳转 /main
    14. this.$router.push("/main");
    15. //console.log(r.data);
    16. })
    17. } else {
    18. console.log('error submit!!');
    19. return false;
    20. }
    21. });
    22. },
    23. }
    1. import Vue from 'vue'
    2. import VueRouter from 'vue-router'
    3. import HomeView from '../views/HomeView.vue'
    4. Vue.use(VueRouter)
    5. const routes = [
    6. {
    7. path: '/',
    8. name: 'home',
    9. component: HomeView
    10. },
    11. {
    12. path: '/login',
    13. name: 'login',
    14. component: () => import(/* webpackChunkName: "about" */ '../views/login.vue')
    15. },
    16. {
    17. path: '/about',
    18. name: 'about',
    19. // route level code-splitting
    20. // this generates a separate chunk (about.[hash].js) for this route
    21. // which is lazy-loaded when the route is visited.
    22. component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
    23. },
    24. {
    25. path: '/main',
    26. name: 'main',
    27. component: () => import(/* webpackChunkName: "about" */ '../views/main.vue')
    28. },
    29. ]
    30. // 针对ElementUI导航栏中重复导航报错问题
    31. const originalPush = VueRouter.prototype.push
    32. VueRouter.prototype.push = function push(location) {
    33. return originalPush.call(this, location).catch(err => err)
    34. }

    这里配置了导航重复导航的问题,我们在响应拦截器配置了code非200的跳转登录的情况,为了避免登录失败导致跳转登录页面,重复导航的问题

    5.后端加入跨域的配置文件

    1. import org.springframework.context.annotation.Bean;
    2. import org.springframework.context.annotation.Configuration;
    3. import org.springframework.web.cors.CorsConfiguration;
    4. import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
    5. import org.springframework.web.filter.CorsFilter;
    6. @Configuration
    7. public class CrossConfig {
    8. @Bean
    9. public CorsFilter corsFilter() {
    10. final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    11. final CorsConfiguration corsConfiguration = new CorsConfiguration();
    12. //corsConfiguration.setAllowCredentials(true); // 允许 携带cookie 的信息
    13. corsConfiguration.addAllowedHeader("*"); // 允许所有的头
    14. corsConfiguration.addAllowedOrigin("*");// 允许所有的请求源
    15. corsConfiguration.addAllowedMethod("*"); // 所欲的方法 get post delete put
    16. source.registerCorsConfiguration("/**", corsConfiguration); // 所有的路径都允许跨域
    17. return new CorsFilter(source);
    18. }
    19. }

    6.统一返回数据实体

    1. @Data
    2. @AllArgsConstructor //
    3. @NoArgsConstructor //
    4. public class Result {
    5. /**
    6. * code编码
    7. */
    8. private Integer code = 200;
    9. /**
    10. * 消息
    11. */
    12. private String msg = "操作成功";
    13. /**
    14. * 具体的数据
    15. */
    16. private T t;
    17. /**
    18. * 成功的静态方法
    19. */
    20. public static Result success(T t){
    21. return new Result<>(200,"操作成功",t);
    22. }
    23. public static Result fail(){
    24. return new Result<>(500,"操作失败",null);
    25. }
    26. public static Result forbidden(){
    27. return new Result<>(403,"权限不允许",null);
    28. }
    29. }

    7.对实现了UserDetailsService接口的service层进行了修改

    1. @Service
    2. public class MyUserDetailService implements UserDetailsService {
    3. @Resource
    4. private TabUserMapper userMapper;
    5. @Resource
    6. private TabUserRoleMapper userRoleMapper;
    7. @Resource
    8. private TabRoleMapper roleMapper;
    9. @Resource
    10. private TabMenuMapper menuMapper;
    11. // 根据用户的名字 加载用户的信息
    12. @Override
    13. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    14. // username 代表前端传递过来的名字
    15. // 根据名字去数据库查询一下有没有这个用户的信息
    16. QueryWrapper queryWrapper = new QueryWrapper();
    17. queryWrapper.eq("username",username);
    18. TabUser tabUser = userMapper.selectOne(queryWrapper);
    19. if(tabUser != null) {
    20. // 有值 查询用户对应的角色的id
    21. QueryWrapper queryWrapper1 = new QueryWrapper();
    22. queryWrapper1.eq("uid",tabUser.getId());
    23. List tabUserRoles = userRoleMapper.selectList(queryWrapper1);
    24. List rids = tabUserRoles.stream().map(tabUserRole -> tabUserRole.getRid()).collect(Collectors.toList());
    25. // 根据角色的id 查询rcode
    26. List tabRoles = roleMapper.selectBatchIds(rids);
    27. // 角色的修信息 角色管理 修改角色的名字
    28. List collect = tabRoles.stream().map(tabRole -> new SimpleGrantedAuthority("ROLE_" + tabRole.getRcode())).collect(Collectors.toList());
    29. // 根据角色的id 查询菜单的mcode
    30. List menus = menuMapper.selectCodeByRids(rids);
    31. List resources = menus.stream().map(tabMenu -> new SimpleGrantedAuthority(tabMenu.getMcode())).collect(Collectors.toList());
    32. // 将角色的所有信息,和资源信息合并在一起
    33. List allresource = Stream.concat(collect.stream(), resources.stream()).collect(Collectors.toList());
    34. return new User(username, tabUser.getPassword(), allresource);
    35. }
    36. return null;
    37. }
    38. }

    8.数据链路层,对前后端的认证进行判断与返回的JSON数据

    1. @Component
    2. public class JwtFilter extends OncePerRequestFilter {
    3. @Override
    4. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    5. /*解析token
    6. 1.获取token -> 存在 -> 解析
    7. 不存在返回 null 没有认证
    8. 2.效验token真的还是假的 真-> 过 -> 用户的信息存放到安全框架的上下文路径里面
    9. 假-> 返回一个Json 数据 没有认证
    10. * */
    11. String[] whitename = {"/userlogin"};
    12. String token = request.getHeader("token");
    13. // token存在
    14. if(StringUtils.isNotBlank(token)) {
    15. // 存在 解析
    16. boolean verify = JWTUtil.verify(token, "hp".getBytes());
    17. if(verify) {
    18. // 效验合格
    19. // 获取用户的名字 和密码的信息
    20. JWT jwt = JWTUtil.parseToken(token);
    21. String username = (String) jwt.getPayload("username");
    22. List resources = (List) jwt.getPayload("resources");
    23. // 资源的信息
    24. List collect = resources.stream().map(s -> new SimpleGrantedAuthority(s)).collect(Collectors.toList());
    25. // 保存用户的信息
    26. UsernamePasswordAuthenticationToken usertoken = new UsernamePasswordAuthenticationToken(username, null, collect);
    27. // 存起来用户的信息
    28. SecurityContextHolder.getContext().setAuthentication(usertoken);
    29. // 放行
    30. filterChain.doFilter(request,response);
    31. }else {
    32. Result result = new Result(401, "没有登录", null);
    33. printJsonData(response,result);
    34. }
    35. }else {
    36. // 查看是否在白名单 如果在 就放行
    37. String requestURL = request.getRequestURI();
    38. if(ArrayUtils.contains(whitename,requestURL)) {
    39. filterChain.doFilter(request,response);
    40. }else {
    41. Result result = new Result(401, "没有登录", null);
    42. printJsonData(response,result);
    43. }
    44. }
    45. }
    46. public void printJsonData(HttpServletResponse response, Result result) {
    47. try {
    48. response.setContentType("application/json;charset=utf8"); //json格式 编码是中文
    49. ObjectMapper objectMapper = new ObjectMapper();
    50. String s = objectMapper.writeValueAsString(result);// 使用objectMapper将result转化为json字符串
    51. PrintWriter writer = response.getWriter();
    52. writer.print(s);
    53. writer.flush();
    54. writer.close();
    55. }catch (Exception e) {
    56. e.printStackTrace();
    57. }
    58. }
    59. }

    9.对config文件进行修改(前后端分离情况)

    1. @Configuration
    2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
    3. @Resource
    4. private JwtFilter jwtFilter;
    5. @Override
    6. protected void configure(HttpSecurity http) throws Exception {
    7. // 配置 登录form 表单
    8. // 路劲前面必须加 /
    9. http.formLogin()
    10. .loginProcessingUrl("/userlogin")
    11. .successHandler((request, response, authentication) -> {
    12. System.out.println("authentication"+authentication);
    13. // 资源的信息
    14. Collectionextends GrantedAuthority> authorities = authentication.getAuthorities();
    15. List allresources = authorities.stream().map(s -> s.getAuthority()).collect(Collectors.toList());
    16. System.out.println("allresources"+allresources);
    17. // 认证成功
    18. // 生成token
    19. Map map =new HashMap<>();
    20. map.put("username",authentication.getName()); // 认证成功之后 用户的名字
    21. map.put("resources",allresources);
    22. // 资源的信息
    23. 设置签发时间
    24. // Calendar instance = Calendar.getInstance(); //获取当前的时间
    25. // Date time = instance.getTime();
    26. 过期的时间设置为2小时之后
    27. // instance.add(Calendar.HOUR,2); //两个小时之后
    28. // Date time1 = instance.getTime();
    29. // map.put(JWTPayload.EXPIRES_AT,time1);
    30. // map.put(JWTPayload.ISSUED_AT,time);
    31. // map.put(JWTPayload.NOT_BEFORE,time);
    32. String token = JWTUtil.createToken(map, "hp".getBytes());
    33. System.out.println(token);
    34. Result result = new Result(200,"登录成功",token);
    35. printJsonData(response,result);
    36. }) //前后端分离的时候 认证成功 走的方法
    37. .failureHandler((request, response, exception) -> {
    38. Result result = new Result(500, "失败", null);
    39. printJsonData(response,result);
    40. }); //认证失败 走的方法
    41. http.authorizeRequests().antMatchers("/userlogin").permitAll(); //代表放行 "/userlogin"
    42. http.authorizeRequests().anyRequest().authenticated();
    43. // 权限不允许的时候
    44. http.exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> {
    45. Result result = new Result(403, "权限不允许", null);
    46. printJsonData(response,result);
    47. });
    48. http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
    49. // csrf 方便html文件 能够通过
    50. http.csrf().disable();
    51. http.cors(); // 可以跨域
    52. }
    53. @Resource
    54. private UserDetailsService userDetailsService;
    55. @Bean
    56. public PasswordEncoder getPassword() {
    57. return new BCryptPasswordEncoder();
    58. }
    59. // 自定义用户的信息
    60. @Override
    61. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    62. auth.userDetailsService(userDetailsService).passwordEncoder(getPassword());
    63. }
    64. public void printJsonData(HttpServletResponse response, Result result) {
    65. try {
    66. response.setContentType("application/json;charset=utf8");
    67. ObjectMapper objectMapper = new ObjectMapper();
    68. String s = objectMapper.writeValueAsString(result);
    69. PrintWriter writer = response.getWriter();
    70. writer.print(s);
    71. writer.flush();
    72. writer.close();
    73. }catch (Exception e) {
    74. e.printStackTrace();
    75. }
    76. }
    77. }

    10.配置完成

  • 相关阅读:
    2D和3D版本的重力游戏
    Word控件Spire.Doc 【加密解密】教程(三):用密码加密 PDF 从 word 到 PDF 转换
    网安学习-内网渗透2
    图解 | 监控系统 Prometheus 的原理
    性能优化:Redis使用优化(1)
    C++未初始化内存出现flashback如何处理
    Kafka 分区机制详解
    树莓派4B无屏幕连接Wi-Fi/启用ssh/创建用户
    package ‘XXXX’ is not available (for R version 3.6.0) 解决R版本适配的问题
    探秘 | 简说IP地址以及路由器的功能究竟是什么?
  • 原文地址:https://blog.csdn.net/Z15800020057/article/details/134311851